Plonger profondément: du CSS au transistor

Il y a 70 ans, le 16 décembre 1947, dans les laboratoires Bell Labs, John Bardin et Walter Brattain, sous la direction de William Shockley, ont créé le premier transistor bipolaire opérationnel. Le 23 décembre, Brattain a présenté à ses collègues le premier amplificateur à transistor. Par conséquent, ce jour est souvent appelé Transistor Day .

Bardin est debout à gauche, Brattain est debout à droite, Shockley est assis

Il n'est pas nécessaire de parler de l'importance de cet événement. Le transistor est considéré comme l'une des inventions les plus importantes du XXe siècle, sans lequel les ordinateurs fonctionneraient encore sur les lampes et les relais et occuperaient des bâtiments entiers. Shockley, Bardin et Brattain ont reçu le prix Nobel de physique pour leur travail en 1956. Au fil des ans, le transistor s'est miniaturisé à seulement quelques atomes. Chaque processeur possède des milliards de transistors, de sorte que le transistor peut être appelé le périphérique le plus massif créé par l'humanité.

Mais quel genre de travail le transistor fait-il pour nous? Allons dans un voyage mental: nous allons tracer le chemin depuis le bout des doigts de haut niveau jusqu'à notre anniversaire - le transistor.

Que prendre comme point de départ? Eh bien, dessinez au moins un bouton habrakat.

HTML et CSS


Un bouton se compose de pixels d'arrière-plan, de texte et d'une bordure. Dans le code, défini par la balise <a>, auquel les règles de disposition CSS sont appliquées. Par exemple, une règle CSS est appliquée à une bordure aux coins arrondis:

border-radius: 3px;
knopa

Ainsi, la frontière se compose de quatre segments et de quatre arcs («quarts» d'un cercle).

Navigateur


Pour la recherche, j'ai pris mon Firefox préféré. Avant que FF ne commence à dessiner notre bouton, il doit faire beaucoup de travail sur l'analyse et le calcul de la position des éléments:

  • Télécharger du HTML sur un réseau, analyser, composer une arborescence DOM
  • Téléchargez sur le réseau CSS, effectuez une analyse lexicale, analysez
  • Lier des règles basées sur la priorité et l'héritage aux éléments de la page
  • Pour tous les nœuds DOM visibles, composez un arbre de leurs zones rectangulaires - cadres.
  • Pour les cadres, calculez les dimensions et l'emplacement (voir la vidéo )
  • Composez des calques à partir de cadres en tenant compte du z-index et du type de contenu (<canvas>, SVG, <video>).
  • Créez une liste de dessins dans l'ordre: couleur d'arrière-plan, image d'arrière-plan, bordure, descendants, contour.


Nous ne nous attarderons pas sur ces étapes en détail. Après eux vient le dessin réel des éléments nécessaires.

Téléchargez la source pour savoir ce qui s'y passe,
Mozilla Firefox. Firefox Mercurial Visual Studio C++. VS symbols.mozilla.org. - /layout/.

, , , Firefox. c , , — FF.

Le fichier nsCSSRenderingBorders.cpp est responsable du dessin des bordures . Et la fonction générale du dessin des bordures s'appelle (qui aurait pensé): DrawBorders () . La fonction sélectionne la méthode de rendu optimale pour diverses situations. Nous avons un cas relativement simple: il y a un rayon de bordure, mais les bordures de tous les côtés sont solides et de la même couleur.

Notre si
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;
  }


Il existe des options beaucoup plus complexes, telles que l'ancrage dans les coins avec le rayon de bordure de différents types de bordures en pointillés et en pointillés, voir DrawDashedOrDottedCorner () . Il en code complètement
grands commentaires
    //      radius.width
    // |<----------------->|
    // |                   |
    // |             ___---+-------------
    // |         __--     #|#       ###
    // |       _-        ##|##     #####
    // |     /           ##+##     ##+##
    // |   /             # P #     #####
    // |  |               #|#       ###
    // | |             __--+-------------
    // ||            _-    ^
    // ||          /       |
    // |         /        first dot is filled
    // |        |
    // |       |
    // |      |
    // |      |
    // |      |
    // +------+
    // |##  ##|
    // |##  ##|
    // |##  ##|


Mais revenons à notre if. À partir du commentaire, nous apprenons que dans ce cas, la bordure est dessinée à l'aide de deux rectangles - interne et externe, puis le chemin créé (chemin) est rempli de la couleur souhaitée.

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

Accédez à AppendRoundedRectToPath () dans gfx / 2d / PathHelpers.cpp.

Encore une fois, nous fixons des points d'arrêt
a9430-clip-21kb

Nous apprenons du commentaire sur la fonction que les coins sont dessinés en quatre points de contrôle par des courbes de Bézier . Les courbes de Bézier sont souvent utilisées en infographie, y compris pour dessiner des arcs de cercles et d'ellipses. Comme nous en apprenons davantage dans le commentaire, il existe de nombreuses options pour choisir des points de contrôle pour construire une courbe. Dans ce cas, nous avons besoin que les points 0 et 3 appartiennent aux côtés du rectangle, les points 0, 1 et C se trouvent sur une ligne droite, les points 3, 2 et C sur l'autre. Voir la figure:

mozilla frontière arrondie courbe de bezier

Il nous reste à calculer le rapport des longueurs des segments 01 / 0C et 32 ​​/ 3C. Ici, les auteurs utilisent des calculs approximatifs et obtiennent la constante magique alpha:

const Float alpha = Float(0.55191497064665766025);

Malheureusement, les articles avec l'algorithme de sélection des points de contrôle référencé par le commentaire ne sont pas dans le domaine public. Mais en général, il convient de noter qu'en infographie, les algorithmes utilisent souvent l'approximation pour améliorer les performances. Par exemple, l' algorithme de Brezenham vous permet de dessiner des segments et des cercles non "dans le front" - en résolvant les équations y = f (x), mais avec des opérations entières plus astucieuses. Même chose avec le remplissage, etc.

Plus loin dans le cycle, nous allons d'un coin à l'autre, utilisons alpha pour calculer les coordonnées des points de contrôle et, enfin, appelons les fonctions de dessin de la ligne de frontière et de l'arc du coin:

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

Ajouter. matériel de lecture

Code AppendRoundedRectToPath () complet
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();
}


Mais alors tout dépend du backend des graphiques 2D que Mozilla utilise.

Moteur graphique


Gecko utilise la bibliothèque Moz2D indépendante de la plate-forme, qui à son tour peut utiliser l'un des backends: Cairo, Skia, Direct2D, Quartz et NV Path. Par exemple, Direct2D, Cairo, Skia sont disponibles pour Windows. Skia est également le backend Chromium. Vous pouvez changer le backend dans about: config. Les backends, à leur tour, peuvent tout lire sur le CPU, ou ils peuvent utiliser l'accélération matérielle GPU dans une certaine mesure. Par exemple, Skia a son propre backend OpenGL - Ganesh.

Le code Direct2D est fermé, nous ferions donc mieux d'activer Skia et de voir ce qu'il fait. La fonction pour dessiner une courbe cubique SkPath :: cubicTo est appelée. Pour construire une courbe, elle est divisée par l'algorithme de Castelljo en un certain nombre de segments droits, qui sont réellement dessinés (voir core / SkGeometry.cpp).


Code machine


Pour être honnête, je n'ai pas réussi à comprendre pleinement les internes de Skia, j'ai donc pris un peu de recul - vers AppendRoundedRectToPath (), où toutes les opérations sont effectuées sur des entiers - ce qui pourrait être plus facile?

Après avoir ouvert le code désassemblé, il faut y trouver l'opération d'addition.

...
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]  
...

Ouais! Même une personne aussi éloignée de l'ASM que je peux facilement deviner que l'opération ADD est responsable de l'ajout. Effectuez la première opération:

142B1863 00 00 add byte ptr [eax],al
0x142B1863 - adresse dans la RAM
0x00 - opcode - code d'instruction du processeur. Ce Mozilla compilé sous x86, et en ouvrant la table d'instructions x86 , nous verrons que le code 00 signifie une opération d'ajout 8 bits avec des mnémoniques ADD. Le premier opérande peut être un registre ou une cellule mémoire à accès aléatoire, le second peut être un registre. Le premier opérande est ajouté au second, le résultat est écrit dans le premier. Je vais expliquer, au cas où, que le registre est une mémoire RAM ultrarapide à l'intérieur du processeur, par exemple, pour stocker des résultats de calcul intermédiaires.

Le deuxième octet est également 0x00 et est appelé MOD-REG-R / M. Ses bits spécifient les opérandes et la méthode d'adressage.



MOD = 00b en combinaison avec R / M = 000b signifie que l' adressage indirect est utilisé
REG = 000b signifie que le registre AL est utilisé (les 8 bits inférieurs du registre EAX)
[eax] - indique que l'ajout est effectué avec la cellule RAM, dont l'adresse est dans le registre EAX.

Comment le processeur traite-t-il la commande ADD?

CPU


Sur la base de la description de la microarchitecture Skylake , j'ai compilé une liste (extrêmement simplifiée) d'étapes:

  1. Les instructions X86 sont extraites d'un cache d'instructions L1 de 32 Ko dans un tampon de précodage de 16 octets
  2. Les commandes précodées sont organisées dans la file d'instructions (taille 2x25) et pénètrent dans les décodeurs
  3. x86 1-4 (µOPs). ADD 1 µOP ALU (- ) 2 µOP AGU ( ) (., ). 86 .
  4. Allocation Queue (IDQ). , Loop Stream Detector — .
  5. : , . . .
  6. La micro-opération est confiée au gestionnaire Unified Scheduler, qui décide à quel moment et à quel port envoyer les opérations à exécuter dans l'ordre de réception. Derrière chaque port se trouve un actionneur. Nos micro-opérations iront à ALU et AGU.


Le cœur de SkyLake. Image de en.wikichip.org .

Je le répète, c'est ma description très simplifiée et ne prétend pas être exacte et complète. Pour plus de référence, je recommande de lire le post Journey Through the Computing Processor Processor et l'article Processors from the Intel Core i7 Family

ALU


Il serait maintenant intéressant de savoir ce qui se passe à ALU: comment les chiffres sont-ils additionnés? Malheureusement, les informations sur la mise en œuvre spécifique de la microarchitecture et de l'ALU sont le secret commercial d'Intel, nous revenons donc à la théorie plus tard.

Un dispositif pour ajouter deux bits binaires (c'est-à-dire un bit) est appelé additionneur . La sortie est la somme et le bit de retenue.


Source: Wikipédia

Depuis dans la vie réelle, nous devons ajouter des nombres composés de plusieurs chiffres, l'additionneur doit également accepter le bit de retenue du chiffre précédent comme entrée. Un tel additionneur est appelé plein .


Source: Wikipedia

Comme le montre la figure, l'additionneur est composé d'éléments logiques: XOR, AND, OR. Et chaque élément logiquepeut être implémenté à l' aide de plusieurs transistors. Ou même un relais .

Mattausch, conception CMOS, H20 / 6/6
Un exemple d'implémentation d'un additionneur complet sur les transistors CMOS . Source

Nous sommes donc arrivés au transistor! Bien que, bien sûr, non seulement les ALU fonctionnent sur les transistors, mais aussi sur d'autres unités de processeur, mais la plupart des transistors sont utilisés dans le cache comme ses cellules.

En réalité, le circuit additionneur de notre processeur peut être construit différemment et être beaucoup plus compliqué. Par exemple, il y a 45 ans déjà, Intel 8008 était en mesure de calculer tous les bits de retenue à l'avance afin d'effectuer l'addition en parallèle (le soi-disant additionneur avec report parallèle). Peu importe, lisez l'intéressant article de blog sur la rétro-ingénierie ALU Intel 8008 dans le blogKen Shirriff. C'est-à-dire Diverses optimisations sont utilisées: par exemple, la multiplication est également bénéfique à ne pas faire de front.

Conclusions: qu'avons-nous appris?


  • C'est compliqué
  • Cela est clairement démontré: pour résoudre le problème de la complexité excessive, les ingénieurs utilisent la division de systèmes complexes en niveaux (couches).
  • Les architectures à plusieurs niveaux offrent une portabilité: par exemple, Firefox peut fonctionner sur différents systèmes d'exploitation et sur différents matériels.
  • L'interaction entre les niveaux est due à l'ouverture des spécifications des interfaces, des services et des formats de données, par exemple HTML et CSS, C ++, un ensemble de commandes x86, etc.
  • Notre héros du jour travaille tout en bas - un transistor .

PS Je suis un amateur (développeur web), et je connais un peu l'architecture C ++, ASM, BT - du cours de l'institut, je pourrais gâcher quelque chose. N'hésitez pas à envoyer des commentaires.

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


All Articles