Mergulho profundo: de CSS a transistor

Há 70 anos, em 16 de dezembro de 1947, nos laboratórios da Bell Labs, John Bardin e Walter Brattain, sob a direção de William Shockley, criaram o primeiro transistor bipolar operacional. Em 23 de dezembro, Brattain demonstrou a seus colegas o primeiro amplificador de transistor. Portanto, esse dia costuma ser chamado de Dia do Transistor .

Bardin está à esquerda, Brattain está à direita, Shockley está sentado

Não há necessidade de falar sobre o significado deste evento. O transistor é considerado uma das invenções mais importantes do século XX, sem as quais os computadores ainda funcionariam com lâmpadas e relés e ocupariam edifícios inteiros. Shockley, Bardin e Brattain receberam o Prêmio Nobel de Física por seu trabalho em 1956. Ao longo dos anos, o transistor miniaturizou para apenas alguns átomos. Cada processador possui bilhões de transistores, de modo que o transistor pode ser chamado de dispositivo mais massivo criado pela humanidade.

Mas que tipo de trabalho o transistor faz por nós? Vamos fazer uma jornada mental: traçaremos o caminho desde as pontas dos dedos de alto nível até o nosso aniversário - transistor.

O que levar como ponto de partida? Bem, pelo menos desenhe um botão habrakat.

HTML e CSS


Um botão consiste em pixels de fundo, texto e borda. No código, definido pela tag <a>, à qual as regras de layout CSS são aplicadas. Por exemplo, uma regra CSS é aplicada a uma borda nos cantos arredondados:

border-radius: 3px;
knopa

Assim, o limite consiste em quatro segmentos e quatro arcos ("quartos" de um círculo).

Navegador


Para pesquisa, peguei meu Firefox favorito. Antes de o FF começar a desenhar o nosso botão, ele precisa trabalhar muito na análise e no cálculo da posição dos elementos:

  • Baixe HTML em uma rede, analise, componha uma árvore DOM
  • Faça o download pela rede CSS, realize análise lexical, analise
  • Vincular regras com base na prioridade e herança aos elementos da página
  • Para todos os nós DOM visíveis, componha uma árvore de suas áreas retangulares - quadros.
  • Para quadros, calcule as dimensões e a localização (consulte o vídeo )
  • Componha camadas de quadros, levando em consideração o índice z e o tipo de conteúdo (<canvas>, SVG, <video>).
  • Crie uma lista de desenhos em ordem: cor de fundo, imagem de fundo, borda, descendentes, estrutura de tópicos.


Não vamos nos aprofundar nessas etapas em detalhes. Depois deles, vem o desenho real dos elementos necessários.

Faça o download da fonte para descobrir o que está acontecendo lá,
Mozilla Firefox. Firefox Mercurial Visual Studio C++. VS symbols.mozilla.org. - /layout/.

, , , Firefox. c , , — FF.

O arquivo nsCSSRenderingBorders.cpp é responsável pelo desenho das bordas . E a função geral de desenhar bordas é chamada (quem teria pensado): DrawBorders () . A função seleciona o método de renderização ideal para várias situações. Temos um caso relativamente simples: existe um raio de borda, mas as bordas de todos os lados são sólidas e da mesma cor.

Nosso se
if (allBordersSame &&
      mCompositeColors[0] == nullptr &&
      mBorderStyles[0] == NS_STYLE_BORDER_STYLE_SOLID &&
      !mAvoidStroke &&
      !mNoBorderRadius)
  {
    // Relatively simple case.
    gfxRect outerRect = ThebesRect(mOuterRect);
    RoundedRect borderInnerRect(outerRect, mBorderRadii);
    borderInnerRect.Deflate(mBorderWidths[eSideTop],
                            mBorderWidths[eSideBottom],
                            mBorderWidths[eSideLeft],
                            mBorderWidths[eSideRight]);

    // Instead of stroking we just use two paths: an inner and an outer.
    // This allows us to draw borders that we couldn't when stroking. For example,
    // borders with a border width >= the border radius. (i.e. when there are
    // square corners on the inside)
    //
    // Further, this approach can be more efficient because the backend
    // doesn't need to compute an offset curve to stroke the path. We know that
    // the rounded parts are elipses we can offset exactly and can just compute
    // a new cubic approximation.
    RefPtr<PathBuilder> builder = mDrawTarget->CreatePathBuilder();
    AppendRoundedRectToPath(builder, mOuterRect, mBorderRadii, true);
    AppendRoundedRectToPath(builder, ToRect(borderInnerRect.rect), borderInnerRect.corners, false);
    RefPtr<Path> path = builder->Finish();
    mDrawTarget->Fill(path, color);
    return;
  }


Existem opções muito mais complexas, como ancorar em cantos com raio de borda de diferentes tipos de bordas pontilhadas e tracejadas, consulte DrawDashedOrDottedCorner () . Existe no código completamente
ótimos comentários
    //      radius.width
    // |<----------------->|
    // |                   |
    // |             ___---+-------------
    // |         __--     #|#       ###
    // |       _-        ##|##     #####
    // |     /           ##+##     ##+##
    // |   /             # P #     #####
    // |  |               #|#       ###
    // | |             __--+-------------
    // ||            _-    ^
    // ||          /       |
    // |         /        first dot is filled
    // |        |
    // |       |
    // |      |
    // |      |
    // |      |
    // +------+
    // |##  ##|
    // |##  ##|
    // |##  ##|


Mas voltando ao nosso se. Com o comentário, aprendemos que, nesse caso, a borda é desenhada usando dois retângulos - interno e externo, e o caminho (caminho) criado é preenchido com a cor desejada.

AppendRoundedRectToPath(builder, mOuterRect, mBorderRadii, true);
AppendRoundedRectToPath(builder, ToRect(borderInnerRect.rect), borderInnerRect.corners, false);
RefPtr<Path> path = builder->Finish();
mDrawTarget->Fill(path, color);

Vá para AppendRoundedRectToPath () em gfx / 2d / PathHelpers.cpp.

Mais uma vez, estabelecemos pontos de interrupção
a9430-clip-21kb

Aprendemos com o comentário sobre a função de que os cantos são desenhados em quatro pontos de controle pelas curvas de Bezier . As curvas de Bezier são frequentemente usadas em computação gráfica, inclusive para desenhar arcos de círculos e elipses. Conforme aprendemos mais com o comentário, há muitas opções para escolher pontos de controle para construir uma curva. Nesse caso, precisamos que os pontos 0 e 3 pertençam aos lados do retângulo, os pontos 0, 1 e C estejam em uma linha reta, os pontos 3, 2 e C na outra. Veja a figura:

mozilla borda arredondada bezier curva

Resta calcular a relação dos comprimentos dos segmentos 01 / 0C e 32 / 3C. Aqui, os autores usam cálculos aproximados e obtêm a constante mágica alfa:

const Float alpha = Float(0.55191497064665766025);

Infelizmente, artigos com o algoritmo de seleção de ponto de verificação referenciado pelo comentário não são de domínio público. Mas, em geral, deve-se notar que, em computação gráfica, os algoritmos costumam usar aproximação para melhorar o desempenho. Por exemplo, o algoritmo de Brezenham permite desenhar segmentos e círculos não "na testa" - resolvendo as equações y = f (x), mas com operações inteiras mais astutas. A mesma coisa com preenchimento, etc.

Além disso, no ciclo, vamos de canto a canto, use alfa para calcular as coordenadas dos pontos de controle e, finalmente, chame as funções de desenhar a linha de borda e o arco do canto:

aPathBuilder->LineTo(p0);
aPathBuilder->BezierTo(p1, p2, p3); 

Adicionar. material de leitura

Código AppendRoundedRectToPath () completo
void
AppendRoundedRectToPath(PathBuilder* aPathBuilder,
                        const Rect& aRect,
                        const RectCornerRadii& aRadii,
                        bool aDrawClockwise)
{
  // For CW drawing, this looks like:
  //
  //  ...******0**      1    C
  //              ****
  //                  ***    2
  //                     **
  //                       *
  //                        *
  //                         3
  //                         *
  //                         *
  //
  // Where 0, 1, 2, 3 are the control points of the Bezier curve for
  // the corner, and C is the actual corner point.
  //
  // At the start of the loop, the current point is assumed to be
  // the point adjacent to the top left corner on the top
  // horizontal.  Note that corner indices start at the top left and
  // continue clockwise, whereas in our loop i = 0 refers to the top
  // right corner.
  //
  // When going CCW, the control points are swapped, and the first
  // corner that's drawn is the top left (along with the top segment).
  //
  // There is considerable latitude in how one chooses the four
  // control points for a Bezier curve approximation to an ellipse.
  // For the overall path to be continuous and show no corner at the
  // endpoints of the arc, points 0 and 3 must be at the ends of the
  // straight segments of the rectangle; points 0, 1, and C must be
  // collinear; and points 3, 2, and C must also be collinear.  This
  // leaves only two free parameters: the ratio of the line segments
  // 01 and 0C, and the ratio of the line segments 32 and 3C.  See
  // the following papers for extensive discussion of how to choose
  // these ratios:
  //
  //   Dokken, Tor, et al. "Good approximation of circles by
  //      curvature-continuous Bezier curves."  Computer-Aided
  //      Geometric Design 7(1990) 33--41.
  //   Goldapp, Michael. "Approximation of circular arcs by cubic
  //      polynomials." Computer-Aided Geometric Design 8(1991) 227--238.
  //   Maisonobe, Luc. "Drawing an elliptical arc using polylines,
  //      quadratic, or cubic Bezier curves."
  //      http://www.spaceroots.org/documents/ellipse/elliptical-arc.pdf
  //
  // We follow the approach in section 2 of Goldapp (least-error,
  // Hermite-type approximation) and make both ratios equal to
  //
  //          2   2 + n - sqrt(2n + 28)
  //  alpha = - * ---------------------
  //          3           n - 4
  //
  // where n = 3( cbrt(sqrt(2)+1) - cbrt(sqrt(2)-1) ).
  //
  // This is the result of Goldapp's equation (10b) when the angle
  // swept out by the arc is pi/2, and the parameter "a-bar" is the
  // expression given immediately below equation (21).
  //
  // Using this value, the maximum radial error for a circle, as a
  // fraction of the radius, is on the order of 0.2 x 10^-3.
  // Neither Dokken nor Goldapp discusses error for a general
  // ellipse; Maisonobe does, but his choice of control points
  // follows different constraints, and Goldapp's expression for
  // 'alpha' gives much smaller radial error, even for very flat
  // ellipses, than Maisonobe's equivalent.
  //
  // For the various corners and for each axis, the sign of this
  // constant changes, or it might be 0 -- it's multiplied by the
  // appropriate multiplier from the list before using.

  const Float alpha = Float(0.55191497064665766025);

  typedef struct { Float a, b; } twoFloats;

  twoFloats cwCornerMults[4] = { { -1,  0 },    // cc == clockwise
                                 {  0, -1 },
                                 { +1,  0 },
                                 {  0, +1 } };
  twoFloats ccwCornerMults[4] = { { +1,  0 },   // ccw == counter-clockwise
                                  {  0, -1 },
                                  { -1,  0 },
                                  {  0, +1 } };

  twoFloats *cornerMults = aDrawClockwise ? cwCornerMults : ccwCornerMults;

  Point cornerCoords[] = { aRect.TopLeft(), aRect.TopRight(),
                           aRect.BottomRight(), aRect.BottomLeft() };

  Point pc, p0, p1, p2, p3;

  if (aDrawClockwise) {
    aPathBuilder->MoveTo(Point(aRect.X() + aRadii[RectCorner::TopLeft].width,
                               aRect.Y()));
  } else {
    aPathBuilder->MoveTo(Point(aRect.X() + aRect.Width() - aRadii[RectCorner::TopRight].width,
                               aRect.Y()));
  }

  for (int i = 0; i < 4; ++i) {
    // the corner index -- either 1 2 3 0 (cw) or 0 3 2 1 (ccw)
    int c = aDrawClockwise ? ((i+1) % 4) : ((4-i) % 4);

    // i+2 and i+3 respectively.  These are used to index into the corner
    // multiplier table, and were deduced by calculating out the long form
    // of each corner and finding a pattern in the signs and values.
    int i2 = (i+2) % 4;
    int i3 = (i+3) % 4;

    pc = cornerCoords[c];

    if (aRadii[c].width > 0.0 && aRadii[c].height > 0.0) {
      p0.x = pc.x + cornerMults[i].a * aRadii[c].width;
      p0.y = pc.y + cornerMults[i].b * aRadii[c].height;

      p3.x = pc.x + cornerMults[i3].a * aRadii[c].width;
      p3.y = pc.y + cornerMults[i3].b * aRadii[c].height;

      p1.x = p0.x + alpha * cornerMults[i2].a * aRadii[c].width;
      p1.y = p0.y + alpha * cornerMults[i2].b * aRadii[c].height;

      p2.x = p3.x - alpha * cornerMults[i3].a * aRadii[c].width;
      p2.y = p3.y - alpha * cornerMults[i3].b * aRadii[c].height;

      aPathBuilder->LineTo(p0);
      aPathBuilder->BezierTo(p1, p2, p3);
    } else {
      aPathBuilder->LineTo(pc);
    }
  }

  aPathBuilder->Close();
}


Mas tudo depende do back-end dos gráficos 2D que a Mozilla usa.

Mecanismo de gráficos


O Gecko usa a biblioteca Moz2D independente da plataforma, que por sua vez pode usar um dos back-ends: Cairo, Skia, Direct2D, Quartz e NV Path. Por exemplo, Direct2D, Cairo, Skia estão disponíveis para Windows. O Skia também é o back-end do Chromium. Você pode alterar o back-end em about: config. Os back-ends, por sua vez, podem ler tudo na CPU, ou podem usar a aceleração de hardware da GPU até certo ponto. Por exemplo, a Skia possui seu próprio back-end OpenGL - Ganesh.

O código do Direct2D está fechado, por isso, é melhor ativar o Skia e ver o que ele faz. A função para desenhar uma curva cúbica SkPath :: cubicTo é chamada. Para construir uma curva, ela é dividida pelo algoritmo de Castelljo em vários segmentos retos, que são realmente desenhados (consulte core / SkGeometry.cpp).


Código da máquina


Para ser sincero, não consegui entender completamente os componentes internos do Skia, então dei um passo atrás - para AppendRoundedRectToPath (), onde todas as operações são executadas em números inteiros - o que poderia ser mais fácil?

Após abrir o código desmontado, precisamos encontrar a operação de adição nele.

...
142B1863 00 00                add         byte ptr [eax],al  
142B1865 00 8D 43 FF 0F 84    add         byte ptr [ebp-7BF000BDh],cl  
142B186B 67 01 00             add         dword ptr [bx+si],eax  
142B186E 00 99 0F 57 C9 F7    add         byte ptr [ecx-836A8F1h],bl  
142B1874 F9                   stc  
142B1875 8B C3                mov         eax,ebx  
142B1877 8B CA                mov         ecx,edx  
142B1879 99                   cdq  
142B187A F7 7C 24 28          idiv        eax,dword ptr [esp+28h]  
...

Sim! Mesmo uma pessoa tão distante do ASM quanto eu posso imaginar facilmente que a operação ADD é responsável pela adição. Faça a primeira operação:

142B1863 00 00 add byte ptr [eax],al
0x142B1863 - endereço na RAM
0x00 - opcode - código de instrução do processador. Este Mozilla compilado sob x86 e abrindo a tabela de instruções x86 , veremos que o código 00 significa uma operação de adição de 8 bits com mnemônicos ADD. O primeiro operando pode ser um registro ou uma célula de memória de acesso aleatório, o segundo pode ser um registro. O primeiro operando é adicionado ao segundo, o resultado é gravado no primeiro. Explicarei, por precaução, que o registro é uma memória RAM ultra-rápida dentro do processador, por exemplo, para armazenar resultados de cálculos intermediários.

O segundo byte também é 0x00 e é chamado MOD-REG-R / M. Seus bits especificam os operandos e o método de endereçamento.



MOD = 00b em combinação com R / M = 000b significa que o endereçamento indireto é usado
REG = 000b significa que o registro AL é usado (os 8 bits mais baixos do registro EAX)
[eax] - indica que a adição é feita com a célula RAM, cujo endereço está no registro EAX.Como

o processador processa o comando ADD?

CPU


Com base na descrição da microarquitetura Skylake , compilei uma lista (extremamente simplificada) de etapas:

  1. As instruções X86 são buscadas de um cache de instruções L1 de 32 KB em um buffer de pré-codificação de 16 bytes
  2. Os comandos pré-codificados são organizados na Fila de instruções (tamanho 2 x 25) e entram nos decodificadores
  3. x86 1-4 (µOPs). ADD 1 µOP ALU (- ) 2 µOP AGU ( ) (., ). 86 .
  4. Allocation Queue (IDQ). , Loop Stream Detector — .
  5. : , . . .
  6. A microoperação vai para o gerente do Unified Scheduler, que decide em que ponto e em qual porta enviar operações para execução fora de ordem de recebimento. Atrás de cada porta está um atuador. Nossas micro operações irão para ALU e AGU.


O núcleo do SkyLake. Imagem de en.wikichip.org .

Repito, esta é minha descrição muito simplificada e não pretende ser precisa e completa. Para referência adicional, recomendo a leitura do post Journey através do pipeline de computação do processador e do artigo Processadores da família Intel Core i7

ALU


Agora, seria interessante saber o que está acontecendo na ALU: como os números são somados? Infelizmente, as informações sobre a implementação específica da microarquitetura e da ALU são o segredo comercial da Intel, por isso voltamos à teoria posteriormente.

Um dispositivo para adicionar dois bits binários (isto é, um bit) é chamado somador . A saída é a soma e o bit de transporte.


Fonte: Wikipedia

Desde na vida real, precisamos adicionar números que consistem em vários dígitos, o somador também deve aceitar o bit de transporte do dígito anterior como entrada. Esse somador é chamado de cheio .


Fonte: Wikipedia

Como pode ser visto na figura, o somador é composto por elementos lógicos: XOR, AND, OR. E todo elemento lógicopode ser implementado usando vários transistores. Ou mesmo um revezamento .

Mattausch, CMOS Design, H20 / 6/6
Um exemplo de implementação de um somador completo em transistores CMOS . Fonte

Então chegamos ao transistor! Embora, é claro, não apenas as ALUs funcionem em transistores, mas também em outras unidades processadoras, mas a maioria dos transistores é usada no cache como suas células.

Na realidade, o circuito somador em nosso processador pode ser construído de maneira diferente e muito mais complicado. Por exemplo, a Intel 8008, há 45 anos, já era capaz de calcular todos os bits de transporte com antecedência para realizar a adição em paralelo (o chamado somador com transporte paralelo). Quem se importa, leia a interessante postagem no blog sobre engenharia reversa ALU Intel 8008 no blogKen Shirriff. I.e. várias otimizações são usadas: por exemplo, a multiplicação também é benéfica para não ser feita "de frente".

Conclusões: o que aprendemos?


  • É complicado
  • É claramente mostrado: para resolver o problema da complexidade excessiva, os engenheiros usam a divisão de sistemas complexos em níveis (camadas).
  • Arquiteturas multinível fornecem portabilidade: por exemplo, o Firefox pode ser executado em vários sistemas operacionais e em diferentes hardwares.
  • A interação entre os níveis se deve à abertura de especificações para interfaces, serviços e formatos de dados, por exemplo, HTML e CSS, C ++, um conjunto de comandos x86, etc.
  • Nosso herói do dia está trabalhando bem no fundo - um transistor .

PS Sou amador (desenvolvedor web) e conheço bastante a arquitetura C ++, ASM, BT - pelo curso do instituto, eu poderia estragar alguma coisa. Por favor, sinta-se livre para enviar comentários.

Source: https://habr.com/ru/post/pt400363/


All Articles