Wikilivres frwikibooks https://fr.wikibooks.org/wiki/Accueil MediaWiki 1.46.0-wmf.24 first-letter Média Spécial Discussion Utilisateur Discussion utilisateur Wikilivres Discussion Wikilivres Fichier Discussion fichier MediaWiki Discussion MediaWiki Modèle Discussion modèle Aide Discussion aide Catégorie Discussion catégorie Transwiki Discussion Transwiki Wikijunior Discussion Wikijunior TimedText TimedText talk Module Discussion module Event Event talk Photographie/Fabricants/Olympus 0 6159 763854 763719 2026-04-17T09:12:31Z Banffy 34456 /* Série OM-System */ 763854 wikitext text/x-wiki {{Ph s Fabricants}} {{EnTravaux}} == À classer == <gallery> Olympus Superzoom 110 BW 1.JPG|Superzoom 110 BW Quick Flash AFL.jpg|Quick flash Olympus SZIII stereo microscope.jpg Olympus Stylus.jpg|Stylus Olympus mju ii.jpg|[[/Olympus Mju II/]] Olympus C-960 Zoom.jpg|C 960 Olympus Superzoom 120TC.jpg|Olympus </gallery> == Appareils 18 x 24 == <gallery> Olympus Pen img 0048.jpg|[[/Olympus Pen|Olympus Pen EE]] Olympus Pen img 1197.jpg|Olympus Pen Olympus pen camera.JPG|Olympus Pen Olympus Pen 6867.jpg Olympus Pen.jpg Olympus Pen 4397.jpg|[[/Olympus Pen révision 3|Olympus Pen révision 3]] (1959) Pen s 130503 019 (8705222446).jpg|[[/Olympus Pen S/]] (vers 1960) Image:IMG.svg|Olympus Pen EM Olympus Pen EE (type 1).jpg|[[/Olympus Pen EE|Olympus Pen EE (type 1)]] (vers 1968) {{25}} Olympus Pen EE-2 241-2599.jpg|[[/Olympus Pen EE-2|Olympus Pen EE-2]] Olympus pen ee3.jpg|[[/Olympus Pen EE-3|Olympus Pen EE-3]] Olympus Pen EE3.jpg|Olympus Pen EE-3 Olympus pen ees.jpg|[[/Olympus Pen EE S|Olympus Pen EE S]] Olympus PEN-EE S (meio quadro).jpg|Olympus Pen EE S Olympus Pen EES2.jpg|[[/Olympus Pen EES-2|Olympus Pen EES-2]] Olympus Pen EED.jpg|Olympus Pen EED Olympus pen eed.jpg|Olympus Pen EED 0607 Olympus EED with lens cap (9122191695).jpg|Olympus Pen EED 0606 Olympus EED no lens cap (9124412452).jpg|Olympus Pen EED MelvL P4290007 (5669556412).jpg|Olympus Pen EED MelvL P4290003 (5669548202).jpg|Olympus Pen EED MelvL P4290009 (5669557980).jpg|Olympus Pen EED MelvL P4290006 (5669403335).jpg|Olympus Pen EED MelvL P4290004 (5668984407).jpg|Olympus Pen EED MelvL P4290008 (5668985957).jpg|Olympus Pen EED MelvL P1040065 (5703691290).jpg|Olympus Pen EED MelvL (5703980674).jpg|Olympus Pen EED MelvL P1040116 (5709471065).jpg|Olympus Pen EED MelvL P1040153 (5730856146).jpg|Olympus Pen EED MelvL P1040155 (5726118704).jpg|Olympus Pen EED Olympus Pen EES-2 (6717094541).jpg|Olympus Pen EES-2 Pen D3.jpg|[[/Olympus Pen D3|Olympus Pen D3]] (1965-1969) Olympus PenF.jpg|[[/Olympus Pen F|Olympus Pen F]] (vers 1963) Olympus-Pen-FT-with-38mm1 8.jpg|[[/Olympus Pen FT|Olympus Pen FT]] (vers 1968) {{75}} </gallery> == Appareils 24 x 36 reflex == <gallery> Olympus FTL front.jpg|[[/Olympus FTL/]] (1971-1972) {{25}} Olympus OM-1 (13573573703).jpg|[[/Olympus OM-1/]] (1973-1974) {{25}} OM1NB 1.jpg|[[/Olympus OM-1N/]] Olympus OM1MD.jpg|[[/Olympus OM-1 MD/]] (avant 1979) {{50}} OM1-n MD (4072626146).jpg|[[/Olympus OM1-n MD/]] (1979-1983) Olympus OM-2 with Zuiko 50mm f1.8.jpg|[[/Olympus OM-2/]] (1976) {{75}} Olympus OM-2N img 0732.jpg|[[/Olympus OM-2N/]] {{25}} Olympus OM-2 SP.jpg|[[/Olympus OM-2 SP/]] {{25}} Olympus OM10 35-70mm.jpg|[[/Olympus OM10/]] (1978) {{100}} Olympusom3.jpg|[[/Olympus OM-3/]] {{25}} Olympus OM3 ti.jpg|OM-3 Ti OM-3Ti Black.jpg Olympus OM3 ti OM4 ti.jpg Olympus OM20 - Tokino 70-210.jpg|[[/Olympus OM20|Olympus OM20 = Olympus OMG (A)]] (1982) Olympus OM-30 (bottom).jpg|[[/Olympus OM30/]] (1982) OlympusOM4 1.JPG|[[/Olympus OM-4/]] {{50}} Olympus OM4 ti 01.jpg|OM-4 Ti Olympus OM-4 Ti.JPG|OM-4 Ti Olympus OM-4Ti worn black body with Zuiko 1.8-50mm lens and neckstrap.jpg Vintage Olympus OM-PC (aka OM-40) 35mm SLR Film Camera, Made In Japan, Circa 1985 (13517132323).jpg|[[/Olympus OM-40|Olympus OM-40 = OM-PC]] (vers 1985) </gallery> == Appareils 24 x 36 compacts == <gallery> Olympus LT1 (3007523325).jpg|[[/Olympus LT1/]] Olympus Superzoom 3000.jpg|Olympus Superzoom 3000 Olympus XA camera and film.jpg|[[/Olympus XA/]] {{25}} My Olympus XA1 (4379061989).jpg|[[/Olympus XA1/]] My Olympus XA2 (4989175842).jpg|[[/Olympus XA2/]] Olympus Ecru.jpg|Olympus Ecru Olympus Ecru (4766955124).jpg|[[/Olympus Ecru/]] série limitée du Mju Olympus Ecru cap.jpg Olympus Ecru back.jpg Olympus Ecru front.jpg Olympus Ecru 01.jpg Olympus Wide.jpg|Olympus wide Olympus Trip 35.jpeg|[[/Olympus Trip 35/]] (vers 1968) {{50}} Olympus-35 ECR.jpg|Olympus-35 ECR Olympus-35 SP.jpg|[[/Olympus 35 SP/]] (1968) {{25}} Olympus35DC3.jpg|35 DC Olympus35DC2.jpg|35 DC Olympus35DC1.jpg|35 DC My Olympus 35DC (4797809987).jpg|35 DC Olympus 35 RC img 1850.jpg|[[/Olympus 35 RC/]] (avant 1977) {{50}} Olympus 35RD.jpg|35 RD Olympus Stylus Epic 1118.jpg|[[/Olympus mju II|Olympus mju II = Olympus Stylus Epic]] {{25}} My Olympus XA-3 (4024574761).jpg|[[/Olympus XA3/]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus XA4 Macro/]] Olympus mju i.jpg|mju 1 Mju (3645746098).jpg 2009-11-26-Olympus-700BF-1.jpg|700 BF 2009-11-26-Olympus-700BF-2.jpg|700 BF 2009-11-26-Olympus-700BF-3.jpg|700 BF Olympus-stylus hg.jpg[Stylus zoom 115 Olympus Superzoom 120 1a.jpg|Superzoom 120 Olympus Superzoom 120TC.jpg|Olympus Superzoom 120TC My Olympus AF-1 Infinity (4876749434).jpg|[[/Olympus AF-1 Infinity/]] Olympus Infinity Jr. (4815671398).jpg|[[/Olympus Infinity Jr./]] Olympus AZ-200 Superzoom.jpg|Olympus AZ-200 Superzoom Olympus Trip MD3.jpg|Olympus Trip MD3 Olympus LT-105Z (6733278979).jpg|Olympus LT-105Z </gallery> == Appareils 24x36 bridge == <gallery> My Olympus IS-1 (4662576887).jpg|[[/Olympus IS-1|Olympus IS-1]] Olympus IS10 (3) (5789273975).jpg|[[/Olympus IS-10|Olympus IS-10]] {{25}} Olympus ED 35-180 (6175609523).jpg|[[/Olympus IS-3000/]] (1993) Olympus-IS-100-07.jpg|[[/Olympus IS-100|Olympus IS-100]] (1994) {{25}} Olympus IS100S (5) (5789275039).jpg|[[/Olympus IS-100S|Olympus IS-100S]] {{25}} Olympus Alvesgaspar.jpg|[[/Olympus IS-1000|Olympus IS-1000]] {{25}} </gallery> == Appareils pour le format AGFA Rapid == <gallery> Image:IMG.svg|[[/Olympus Pen RAPID EES|Olympus Pen RAPID EES]] Image:IMG.svg|[[/Olympus Pen RAPID EED|Olympus Pen RAPID EED]] </gallery> == Appareils pour le format 126 == <gallery> Olympus Quickmatic 600 (2759484117).jpg|[[/Olympus Quickmatic 600|Olympus Quickmatic 600]] </gallery> == Appareils pour le format APS == <gallery> Olympus i zoom 2000 (3854940049).jpg|[[/Olympus i zoom 2000/]] (2000) </gallery> == Appareils numériques non reflex == === année 1996 === <gallery> Image:IMG.svg|[[/Olympus D-200L|Olympus D-200L]] {{50}} (5&nbsp;septembre 1996) Image:IMG.svg|[[/Olympus D-300L|Olympus D-300L]] {{50}} (5&nbsp;septembre 1996) </gallery> === année 1997 === <gallery> Image:Olympus C-820L.jpg|[[/Olympus Camedia C-820L|Olympus Camedia C-820L]] {{50}} (septembre 1997) File:2009-11-26-Olympus-C-820L-1.jpg|C-820L File:2009-11-26-Olympus-C-820L-2.jpg|C-820L File:2009-11-26-Olympus-C-820L-3.jpg|C-820L File:2009-11-26-Olympus-C-820L-5.jpg|C-820L File:2009-11-26-Olympus-C-820L-6.jpg|C-820L File:2009-11-26-Olympus-C-820L-4.jpg|C-820L File:Camedia-C-820L-05.jpg|C-820L File:Camedia-C-820L-02.jpg|C-820L </gallery> === année 1998 === <gallery> Image:IMG.svg|[[/Olympus D-340L|Olympus D-340L]] {{50}} (28&nbsp;septembre 1998) File:Olympus C-900 ZOOM.jpg|[[/Olympus D-400|Olympus D-400 = Stylus Digital 400 = Olympus C900Z)]] {{75}} (2&nbsp;novembre&nbsp;1998) </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus D-340R|Olympus D-340R]] {{50}} (2&nbsp;janvier 1999) File:Olympus Camedia C-2000 Z.jpg|[[/Olympus C-2000 Zoom|Olympus C-2000 Zoom]] {{50}} (16&nbsp;février 1999) Image:IMG.svg|[[/Olympus C-21|Olympus C-21]] {{50}} (28&nbsp;juin&nbsp;1999) File:Olympus Camedia C-21T.commu CP+ 2011.jpg|Olympus Camedia C-21T.commu Image:IMG.svg|[[/Olympus D-450 Zoom|Olympus D-450 Zoom = Olympus C920Z]] {{50}} (31&nbsp;juillet 1999) Image:IMG.svg|[[/Olympus C-2020 Zoom|Olympus C-2020 Zoom]] (19&nbsp;octobre&nbsp;1999) </gallery> === année 2000 === <gallery> Olympos-Camedia-C3000.jpg|C3000 Image:IMG.svg|[[/Olympus C-3030 Zoom|Olympus C-3030 Zoom]] (27&nbsp;janvier&nbsp;2000) Image:IMG.svg|[[/Olympus D-360L|Olympus D-360L]] (2&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-460 Zoom|Olympus C-460 Zoom]] (8&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-3000 Zoom|Olympus C-3000 Zoom]] (24&nbsp;avril&nbsp;2000) Image:Olympus UZ-2100 03.jpg|[[/Olympus C-2100 Ultra Zoom|Olympus C-2100 Ultra Zoom]] (15&nbsp;juin&nbsp;2000) Image:Olympus UZ-2100 01.jpg Image:Olympus UZ-2100 02.jpg Image:IMG.svg|[[/Olympus D-490 Zoom|Olympus D-490 Zoom]] (1er&nbsp;août&nbsp;2000) File:Olympus E100RS.jpg|[[/Olympus E-100 RS|Olympus E-100 RS]] (22&nbsp;août&nbsp;2000) File:Olympus Camera E-100RS.jpg|E-100 RS Image:IMG.svg|[[/Olympus C-3040 Zoom|Olympus 3-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) Image:IMG.svg|[[/Olympus C-2040 Zoom|Olympus C-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) </gallery> === année 2001 === <gallery> Olympus Camedia C-1.jpg|[[/Olympus C-1|Olympus C-1]] (6&nbsp;mars&nbsp;2001) Olympus C-700 Ultra Zoom.jpg|[[/Olympus C-700 Ultra Zoom|Olympus C-700 Ultra Zoom]] (19&nbsp;mars&nbsp;2001) IMG.svg|[[/Olympus D-150 Zoom|Olympus D-150 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-510 Zoom|Olympus D-510 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-370|Olympus D-370]] (5&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus C-4040 Zoom|Olympus C-4040 Zoom]] (20&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus D-40 Zoom|Olympus D-40 Zoom]] (2&nbsp;septembre&nbsp;2001) Olympus Camedia C-2.jpg|[[/Olympus C-2|Olympus C-2]] (13&nbsp;septembre&nbsp;2001) Olympus Camedia C-3020.jpg|[[/Olympus C-3020 Zoom|Olympus C-3020 Zoom]] (15&nbsp;octobre&nbsp;2001) </gallery> === année 2002 === <gallery> File:My Olympus D-520Z (4794377895).jpg|[[/Olympus D-520 Zoom|Olympus D-520 Zoom]] (13&nbsp;mars&nbsp;2002) File:Olympus D-380.jpg|[[/Olympus D-380|Olympus D-380 = Olympus C-120]] (13&nbsp;mars&nbsp;2002) Olympus C-2020Z.jpg|Olympus Camedia C-2020Z OlympusC220ZoomCamera.jpg|C220Z File:Olympus Camedia C-720.jpg|[[/Olympus C-720 Ultra Zoom|Olympus C-720 Ultra Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-300 Zoom|Olympus C-300 Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-4000 Zoom|Olympus C-4000 Zoom]] (25&nbsp;juillet&nbsp;2002) File:Olympus C-5050Z, -Apr. 2007 a.jpg|[[/Olympus C-5050 Zoom|Olympus C-5050 Zoom]] (19&nbsp;août&nbsp;2002) File:Olympus C-5050Z, -6 Aug. 2006 a.jpg|C-5050 File:Olympus C-5050Z, -19 Nov. 2005 a.jpg|C-5050 Image:Olympus C-730UZ Front Left.jpg|[[/Olympus C-730 UZ|Olympus C-730 UZ]] (12&nbsp;septembre&nbsp;2002) Image:IMG.svg|[[/Olympus C-50 Zoom|Olympus C-50 Zoom]] (24&nbsp;septembre&nbsp;2002) </gallery> === année 2003 === <gallery> Image:IMG.svg|[[/Olympus Stylus 400|Olympus Stylus 400 =&nbsp;Olympus µ 400 Digital]] (9&nbsp;janvier&nbsp;2003) Image:IMG.svg|[[/Olympus Stylus 300|Olympus Stylus 300 =&nbsp;Olympus µ 300 Digital]] (9&nbsp;janvier&nbsp;2003) Fichier:Olympus Camedia C-740 Ultra Zoom 10.JPG|[[/Olympus C-740 Ultra Zoom|Olympus C-740 Ultra Zoom]] {{75}} (2&nbsp;mars&nbsp;2003) File:Olympus C-150.JPG|[[/Olympus Camedia C-150|Olympus Camedia C-150 =&nbsp;Olympus D-390]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -3.JPG|[[/Olympus D-560 Zoom|Olympus D-560 Zoom = Camedia C-350 zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -2.JPG Image:Olympus Camedia C-350 Zoom -1.JPG Image:Olympus Camedia C-350 Zoom.JPG Image:Olympus-C350Z.jpg Image:Olympus C-750.jpg|[[/Olympus C-750 Ultra Zoom|Olympus C-750 Ultra Zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus C-750 back.jpg Image:Olympus C-750 front right-1.jpg Image:Olympus C-750 front right.jpg Image:Olympus C-750 front left.jpg Image:Digital Camera.jpg|[[/Olympus C-5000 Zoom|Olympus C-5000 Zoom]] (29&nbsp;août&nbsp;2003) Olympus Camedia C-5000Z 3750.jpg Olympus Camedia C-5000Z 3751.jpg Olympus Camedia C-5000Z 3752.jpg Olympus Camedia C-5000Z 3753.jpg Olympus Camedia C-5000Z 3754.jpg Olympus Camedia C-5000Z 3755.jpg Olympus Camedia C-5000Z 3756.jpg Image:IMG.svg|[[/Olympus C-5060 Zoom|Olympus C-5060 Zoom]] (29&nbsp;septembre&nbsp;2003) </gallery> === année 2004 === <gallery> IMG.svg|[[/Olympus D-540 Zoom|Olympus D-540 Zoom]] (14&nbsp;février&nbsp;2004) IMG.svg|[[/Olympus D-580 Zoom|Olympus D-580 Zoom]] (14&nbsp;février&nbsp;2004) Stylus410specs.jpg|[[/Olympus Stylus 410|Olympus Stylus 410]] (14&nbsp;février&nbsp;2004) OLYMPUS C-8080WZ 01.jpg|[[/Olympus C-8080 WideZoom|Olympus C-8080 WideZoom]] (14&nbsp;février&nbsp;2004) Olympus CAMEDIA C-8080.JPG|C-8080 C-8080WZ rear.JPG|C-8080 C-8080WZ tele.JPG|C-8080 Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus C-765UZ, -13 juni 2006 a.jpg|[[/Olympus C-765 Ultra Zoom|Olympus C-765 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus C-766 UZ back.jpg Olympus C-765 UZ front.jpg IMG.svg|[[/Olympus C-770 Ultra Zoom|Olympus C-770 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus D-395.JPG|[[/Olympus D-395|Olympus D-395]] (18&nbsp;mars&nbsp;2004) Olympus C-60 Zoom.JPG|[[/Olympus C-60 Zoom|Olympus C-60 Zoom]] (18&nbsp;mars&nbsp;2004) Olympus µ-mini.jpeg|[[/Olympus Stylus Verve|Olympus Stylus Verve = Olympus Mju-mini = Olympus mju-ii]] (3&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus C-7000 Zoom|Olympus C-7000 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus D-535 Zoom|Olympus D-535 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus Stylus 500|Olympus Stylus 500]] (29&nbsp;novembre&nbsp;2004) </gallery> === année 2005 === <gallery> IMG.svg|[[/Olympus D-425/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-7070 Wide Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-5500 Sport Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus Stylus Verve S/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-545 Zoom/]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 3.JPG|[[/Olympus D-595 Zoom|Olympus D-595 Zoom = Olympus C-500Z]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 2.JPG Olympus C-500Z 1.JPG Olympus IR-300.jpg|[[/Olympus IR-300/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-630 Zoom/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus Stylus 800/]] (12&nbsp;mai&nbsp;2005) IMG.svg|[[/Olympus D-435/]] (20&nbsp;mai&nbsp;2005) Olympus FE 110 (2254131662).jpg|[[/Olympus FE-110/]] (29&nbsp;août&nbsp;2005) Olympus-SP-310-p1030353.jpg|[[/Olympus SP-310/]] {{00}} (29&nbsp;août&nbsp;2005) Olympus FE-120 01.jpg|[[/Olympus FE-120/]] {{50}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus Stylus 600/]] (29&nbsp;août&nbsp;2005) Oly SP-350-1.jpg|[[/Olympus SP-350/]] {{25}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-500 UZ/]] {{25}} (29&nbsp;août&nbsp;2005) Olympus FE-100 front.jpg|[[/Olympus FE-100/]] (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-700/]] (4&nbsp;octobre&nbsp;2005) </gallery> === année 2006 === <gallery> File:My Olympus SP-320 (4171943306).jpg|[[/Olympus SP-320|Olympus SP-320]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-115|Olympus FE-115]] (26&nbsp;janvier&nbsp;2006) {{25}} File:Olympus-digitale-camera-FE-130.JPG|[[/Olympus FE-130|Olympus FE-130]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-140|Olympus FE-140]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:IMG.svg|[[/Olympus FE-150|Olympus FE-150]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:Olympus µ 700.jpg|[[/Olympus Stylus 700|Olympus Stylus 700 = Mju 700 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 720 SW|Olympus Stylus 720 SW = Olympus Mju 720 SW Digital]] (26&nbsp;janvier&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 810|Olympus Stylus 810 = Olympus Mju 810 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:Olympus X760 01.jpg|[[/Olympus FE-170|Olympus FE-170 = Olympux X-760]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus FE-180|Olympus FE-180]] (24&nbsp;août&nbsp;2006) {{25}} Image:Olympus FE190.JPG|[[/Olympus FE-190|Olympus FE-190]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-200|Olympus FE-200]] (24&nbsp;août&nbsp;2006) {{50}} File:Olympus μ 725 SW.jpg|[[/Olympus Stylus 725 SW|Olympus Stylus 725 SW = Olympus Mju 725 SW Digital]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 730|Olympus Stylus 730 = Olympus Mju 730 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 740|Olympus Stylus 740 = Olympus Mju 740 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 750|Olympus Stylus 750 = Olympus Mju 750 Digital]] (24&nbsp;août&nbsp;2006) {{75}} Image:IMG.svg|[[/Olympus Stylus 1000|Olympus Stylus 1000 = Olympus Mju 1000 Digital]] (24&nbsp;août&nbsp;2006) {{75}} File:OlympusSP510UZ.jpg|[[/Olympus SP-510 UZ|Olympus SP-510 UZ]] (24&nbsp;août&nbsp;2006) {{50}} </gallery> === année 2007 === <gallery> Image:Olympus SP 550UZ.jpg|[[/Olympus SP-550 UZ|Olympus SP-550 UZ]] (25&nbsp;janvier&nbsp;2007) {{100}} File:Fe-210.png|[[/Olympus FE-210|Olympus FE-210 = X-775]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-230|Olympus FE-230]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-240|Olympus FE-240]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-250|Olympus FE-250]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Olympus µ 760.jpg|[[/Olympus Stylus 760|Olympus Stylus 760 = Olympus mju 760 Digital]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Stylus 770SW.jpg|[[/Olympus Stylus 770 SW|Olympus Stylus 770 SW = Olympus mju 770 SW Digital]] (25&nbsp;janvier&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 780|Olympus Stylus 780]] (5&nbsp;mars&nbsp;2007) Image:IMG.svg|[[/Olympus FE-270|Olympus FE-270]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-280|Olympus FE-280]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-290|Olympus FE-290]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-300|Olympus FE-300]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 790 SW|Olympus Stylus 790 SW = Olympus mju 790 SW Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 820|Olympus Stylus 820 = Olympus mju 820 Digital]] (23&nbsp;août&nbsp;2007) {{75}} File:OLYMPUS Mu 830.jpeg|[[/Olympus Stylus 830|Olympus Stylus 830 = Olympus mju 830 Digital]] (23&nbsp;août&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 1200|Olympus Stylus 1200 = Olympus mju 1200 Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus SP-560 UZ|Olympus SP-560 UZ]] (25&nbsp;janvier&nbsp;2007) {{75}} </gallery> === année 2008 === <gallery> File:Olympus SP-570UZ, -Nov. 2008 a.jpg|[[/Olympus SP-570 UZ|Olympus SP-570 UZ]] (2008) {{50}} Image:IMG.svg|[[/Olympus Mju 1040|Olympus Mju 1040]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1050sw|Olympus Mju 1050sw]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1060|Olympus Mju 1060]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-20|Olympus FE-20]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-360|Olympus FE-360]] (19&nbsp;août&nbsp;2008) {{75}} Image:IMG.svg|[[/Olympus FE-370|Olympus FE-370]] (2008) {{25}} </gallery> === année 2009 === <gallery> Bfishadow Olympus E-P1.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 bottom.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 top.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 back.jpg|Olympus Pen E-P1 Olympus Pen img 3486.jpg|Olympus Pen E-P1 Olympus IMG 2163.jpg|Olympus Pen E-P1 Olympus E-P1 (3634318402).jpg Olympus E-P1 (3634895524).jpg Olympus E-P1 (3634080929).jpg Olympus E-P1- Sleek frame (3634087049).jpg Olympus E-P1 (3634889300).jpg Olympus E-P1 (3634079289).jpg Olympus E-P1 (3633504211).jpg Olympus E-P1 (3634318984).jpg Olympus E-P1 (3634318918).jpg Olympus E-P1 (3633503717).jpg </gallery> === année 2010 === <gallery> File:Olympus PEN E-PL1.jpg|[[/Olympus PEN E-PL1|Olympus PEN E-PL1]] (10&nbsp;février&nbsp;2010) {{100}} </gallery> === année 2011 === <gallery> File:Olympus E-PL2 with Leica Summicron 50 f2 LTM lens.jpg|[[/Olympus Pen E-PL2|Olympus Pen E-PL2]] (6&nbsp;janvier&nbsp;2011) {{75}} Image:IMG.svg|[[/Olympus SP-610UZ|Olympus SP-610UZ]] (6&nbsp;janvier&nbsp;2011) {{75}} File:Olympus-XZ-1.jpg|[[/Olympus XZ-1|Olympus XZ-1]] (6&nbsp;janvier&nbsp;2011) {{100}} </gallery> === année 2012 === <gallery> Image:IMG.svg|[[/Olympus Tough TG-1 iHS|Olympus Tough TG-1 iHS]] (8 mai 2012) {{75}} </gallery> === année 2013 === <gallery> </gallery> === année 2014 === <gallery> </gallery> === année 2015 === <gallery> Olympus OM-D E-M5II (18421339075).jpg|[[/Olympus OM-D E-M5 II]] (5&nbsp;février&nbsp;2015) {{100}} IMG.svg|[[/Olympus Tough TG-4/]] (13&nbsp;avril&nbsp;2015) {{75}} Olympus OM-D E-M10 Mark II.JPG|[[/Olympus OM-D E-M10 II/]] (25&nbsp;août&nbsp;2015) {{75}} </gallery> === année 2016 === <gallery> Olympus PEN-F.jpg|[[/Olympus Pen-F/]] (27&nbsp;janvier&nbsp;2016) {{00}} </gallery> ==== à classer ==== <gallery> 2013-265-121 Tough New Toy (8700211111).jpg|Olympus Tough TG-2 OLYMPUS PEN MINI E-PM2 (8651180173).jpg|Olympus E-PM2 Olympus E-20P 01.jpg|Olympus E-20P Olympus E-20P 02.jpg Olympus E-20P 03.jpg Olympus E-20P 04.jpg Olympus E-20P 05.jpg Olympus E-20P 06.jpg Olympus E-20P 07.jpg Olympus E-20P 08.jpg Olympus E-20P 09.jpg Olympus E-20P 10.jpg Olympus E-20P 11.jpg Olympus X-750.jpg|Olympus X-750 Olympus PEN F.jpg|Olympus PEN F Olympus Pen F (digital).jpg|Olympus PEN F Olympus Pen F-IMG 9925.jpg|Olympus PEN F Olympus Pen F-IMG 9927.JPG|Olympus PEN F Olympus PEN F.jpg|Olympus PEN F Olympus PEN-F.jpg|Olympus PEN F Olympus PEN E-PL6 black kit lens 2016-03-03.jpg|Olympus PEN E-PL6 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15772393942).jpg|Olympus OM-D E-M1 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15770833985).jpg OM D E-M1 with 75mm f-1.8 (9869006524).jpg OM D E-M1 with 12-60mm f-2.8-4 ED SWD (9869086256).jpg Olympus OM-D E-M1 Mark II mock-up (rough model) 2017 CP+.jpg|Olympus OM-D E-M1 Mark II Olympus OM-D E-M1 Mark II mock-up (design model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II mock-up (body+power battery holder model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II magnesium-alloy chassis 2017 CP+.jpg Olympus OM-D E-M1 Mark II D81 8378-2.jpg Olympus.OM-D.E-M1.Mark.II.back.view.jpg Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus camedia C-800L.jpg|[[/Olympus Camedia C-800L/]] Olympus SH-1 zilver, -2015 a.jpg|Olympus SH-1 Olympus SH-1 zilver, -2015 b.jpg Olympus SH-1 zilver, -2015 c.jpg Olympus Pen EPL7.JPG|[[/Olympus Pen E-PL7/]] Digitalkamera von Olympus.JPG|FE-5020 Olympus XZ-10, -februari 2013 a.jpg|Olympus XZ-10 Leong IMG 2989 (6338669929).jpg Leong IMG 2986 (6339413622).jpg Leong IMG 0497 (6689370435).jpg P1000963 (6707919805).jpg Olympus OM-D E-M10 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 01.jpg Olympus OM-D E-M10 cutaway 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 cutted 1.jpg Olympus OM-D E-M10 cutted 2.jpg Olympus PEN E-P5 Back 1.jpg|Olympus PEN E-P5 Olympus PEN E-P5 Front 1.jpg Olympus PEN E-P5 Front 2.jpg EP5 with 45mm F1.8.jpg Olympus E-P5.jpg Olympus X-500 D-590Z C-470Z.jpg|X-500 = D-590Z = C-470Z Olympus E-PM1 + BCL-15.jpg|Olympus E-PM1 Olympus TG-820 Camera.jpg|Olympus TG-820 Olympus TG-820 front with lens cover open.jpg|Olympus TG-820 Olympus TG-820 Top.JPG|Olympus TG-820 Olympus E-P2.jpg|Olympus E-P2 Micro Four Thirds Olympus E-P2 with Panasonic Lumix G 20mm F1.7 ASPH aspherical pancake lens.jpg Olympus EPL5 top.jpg|E-PL5 Olympus EPL5 back 01.jpg|E-PL5 Olympus EPL5 back 02.jpg|E-PL5 Olympus EPL5 front lens.jpg|E-PL5 Olympus EPL5 front.jpg|E-PL5 Olympus epl5 vf4.jpg Olympus epl5 vf4 45mm 01.jpg Olympus epl5 vf4 45mm 02.jpg Olympus epl5 vf4 45mm 03.jpg Olympus PEN Lite and Nissin i40.jpg Olympus VR-340.JPG|Olympus VR-340 Olympus-digitale-camera-X-720.JPG|Olympus X-720 Olympus Camedia C-310 Zoom Digital Camera.JPG|Olympus Camedia C-310 Zoom Olympus PEN E-PL3.jpg|[[/Olympus Pen E-PL3|Olympus Pen E-PL3]] Olypmus SP-810UZ, closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_no_zoom,_flash_closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_full_zoom,_flash_opened.jpg|Olympus SP-810UZ Olympus E-P3 006.JPG|[[/Olympus E-P3/]] Olympus AZ-300 Superzoom.jpg|[[/Olympus AZ-300 Superzoom/]] Olympus SP560UZ DSCF9120.jpg|SP560 UZ Olympus u5000.jpg|Mju 5000 Stylus Tough 8000.jpg|Stylus Tough 8000 Olympus_SP590_UZ_01_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_02_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_03_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_04_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_05_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_06_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_07_(RaBoe).jpg|SP590 UZ Olympus SP590 UZ 2010-by-RaBoe-02.jpg Olympus SP590 UZ 2010-by-RaBoe-01.jpg FE-310.jpg|FE-310 Image:Olympus u850SW.jpg|mju 850SW Olympus µ850SW, -1 mei 2010 a.jpg Image:Olympus img 1845.jpg|C-1400 Image:Olympus_FE-340_8MP_camera_01.jpg|FE-340 Olympus-MicroFT-Model.jpg|Micro four thirds OlyE-P2Test10112009-01.jpg|EP-2 Olympus VR-310.jpg|Olympus VR-310 </gallery> == Appareils reflex numériques == === année 1997 === <gallery> Image:IMG.svg|[[/Olympus D-500L|Olympus D-500L]] {{50}} (10&nbsp;septembre 1997) Image:IMG.svg|[[/Olympus D-600L|Olympus D-600L]] {{50}} (10&nbsp;septembre 1997) Image:Olympus img 1846.jpg|C-1400 Image:Olympus img 1845.jpg|C-1400 </gallery> === année 1998 === <gallery> Image:Olympus C-1400 01.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 61.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 57.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus C-2500 L|Olympus C-2500 L]] {{50}} (18&nbsp;mars 1999) </gallery> === année 2000 === <gallery> File:Olympus E-10.jpg|[[/Olympus E-10|Olympus E-10]] (22&nbsp;août&nbsp;2000) File:Olympus E-20P.JPG|[[/Olympus E-20|Olympus E-20]] Olympus E-20n.jpg|Olympus E-20n </gallery> === année 2003 === <gallery> Image:Olympus E-1 2.jpg|[[/Olympus E-1|Olympus E-1]] (24&nbsp;juin&nbsp;2003) Image:E-1 hinten oben.jpg Image:E-1 Seite hinten.jpg Image:E-1 hinten.jpg File:E-1 vorne.jpg File:Olympus E-1 body.jpg </gallery> === année 2004 === <gallery> E-300.jpg|[[/Olympus E-300|Olympus E-300 (EVOLT E-300)]] (27&nbsp;septembre&nbsp;2004) </gallery> === année 2005 === <gallery> File:E-500 Body.jpg|[[/Olympus E-500|Olympus E-500]] {{25}} (2005) Olympus E-500 with Minolta MD Lens (5391265164).jpg|[[/Olympus E-500/]] (26&nbsp;septembre&nbsp;2005) </gallery> === année 2006 === <gallery> File:Olympus E-330. Zuiko Digital ED.jpg|[[/Olympus E-330|Olympus E-330 = EVOLT E-330]] {{25}} (26&nbsp;janvier&nbsp;2006) Image:Oly e 400 voorkant.jpg|[[/Olympus E-400|Olympus E-400]] {{75}} (14&nbsp;septembre&nbsp;2006) </gallery> === année 2007 === <gallery> Olympus E410 img 1030.jpg|[[/Olympus E-410/]] (5&nbsp;mars&nbsp;2007) Olympus E510 img 1029.jpg|[[/Olympus E-510/]] (5&nbsp;mars&nbsp;2007) P3069465 (3333310515).jpg|[[/Olympus E-3/]] Olympus_E-3_Camera.jpg|[[/Olympus E3/]] {{75}} (16&nbsp;octobre&nbsp;2007) </gallery> === année 2008 === <gallery> Olympus E-420.jpg|E-420 Olympus E-420 EZ40150.jpg|E-420 Olympus E-420 Body Front.jpg|E-420 Olympus E-420 Pancake25mm Top.jpg|E-420 Olympus E-420 Body Top XL.jpg|E-420 Image:Olymous E420 img 1248.jpg|E-420 Image:Olympus E-420 (back).jpg|E-420 Image:Olympus E-420 (front).jpg|E-420 </gallery> === année 2009 === <gallery> Olympus E-450.JPG|[[/Olympus E-450|Olympus E-450]] {{75}} (31&nbsp;mars&nbsp;2009) Olympus E-30 01.jpg|Olympus E-30 Olympus E-30 02.jpg|Olympus E-30 Olympus E-30 03.jpg|Olympus E-30 Olympus E-30 04.jpg|Olympus E-30 Olympus E-30 rear01.jpg|E30 Olympus E-30 front01.jpg|E30 E-30-back.jpg|E3 Olympus E-30 with ZD 14-54mm f2.8-3.5II 01.JPG|E30 Olympus E-30-Cutmodel.jpg|E30 coupé E-30-with-14-54.jpg|E30 Olympus E30-IMG 2445.jpg|E30 Olympus E30-IMG 2442.jpg|E30 Olympus E30-IMG 2441.jpg|E30 Olympus E-620 front.jpg|E-620 Olympus E-620.jpg|E-620 Olympus E-620 with battery grip.jpg|E-620 Olympus E-620 without lens.jpg|E620 Olympus E-620 swivel screen open.JPG|E620 Olympus E620 DSLR.jpg|E620 </gallery> === année 2010 === <gallery> Olympus-E5.jpg|E-5 </gallery> '''à classer''' <gallery> Olympus OM-D E-M1- 20131118.jpg|Olympus OM-D E-M1 OM-D EM-1.jpg|OM-D EM-1 Oly-EM1-connector.jpg Olympus OM-D E-M1 01.jpg Olympus OM-D E-M1 cutted 1.jpg Olympus OM-D E-M1 cutted 2.jpg Olympus OM-D E-M1 cutted 3.jpg Olympus OM-D E-M1 image stabilization unit.jpg Olympus E-M5 (front, cropped).jpg|OM-D E-M5 Oly-E-M5.jpg OLYMPUS OM-D E-M5.jpg Olympus OM-D E-M5.jpg Olympus E-M5, Nokton 25mm.jpg Olympus E-M5 01.jpg Olympus E-M5 02.jpg Olympus E-M5 03.jpg Olympus E-M5 04.jpg Olympus E-M5 05.jpg Olympus E-M5 06.jpg Olympus E-M5 08.jpg Olympus E-M5 07.jpg Olympus E-M5 09.jpg Olympus E-M5 10.jpg Olympus E-M5 11.jpg Olympus E-M5 12.jpg Olympus E-M5 13.jpg Olympus E-M5 14.jpg Olympus E-M5 15.jpg Olympus E-M5 16.jpg Olympus OM-D E-M5, Taipei, TW.jpg Olympus E-M5 + Bigma.jpg Micro Four Thirds Olympus OM-D E-M5 digital camera.jpg Oly-E-M5.jpg Olympus OM-D E-M5 Elite black kit.jpg Olympus OM-D E-M5 Elite black.jpg Olympus E-M5 with 45mm F1.8.jpg Olympus OM 50mm f1.8.jpg|E-420 Olympus-e-520-front.png|[[/Olympus E-520|Olympus E-520]] </gallery> == Modules pour smartphone == <gallery> Olympus Air A01,mounted lens and phone.jpg|Olympus Air A01 </gallery> == Objectifs à mise au point manuelle == === Série Pen === <gallery> Olympus-20mm-F3 5-with-TTL-No.jpg|[[/Olympus Zuiko 20 mm f/3,5/]] Image:PenF-Zuiko-20mm.JPG </gallery> === Série FTL (M42) === <gallery> M42.OffenblendOLYMPUS.jpg|Olympus M 42 Olympus FTL G.Zuiko 28 mm f 3,5.jpg|Olympus G.Zuiko 28 mm f/3,5 IMG.svg|Olympus Zuiko 35 mm f/2,8/ IMG.svg|Olympus Zuiko 50 mm f/1,4/ Olympus FTL F. Zuiko 50 mm f 1,8.jpg|Olympus Zuiko 50 mm f/1,8 IMG.svg|Olympus Zuiko 135 mm f/3,5/ IMG.svg|Olympus Zuiko 200 mm f/4,0/ </gallery> === Série OM-System === <gallery> OMLenses.jpg OM Zuiko f2 lenses.jpg|Zuiko f/2 </gallery> <gallery> IMG.svg|[[/Olympus Zuiko 8 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 16 mm f/3,5/]] (avant 1978) {{25}} Olympus Zuiko Auto-Macro 20mm 1-2 lens (4243439258).jpg|[[/Olympus Zuiko Auto-Macro 20 mm f/1,2/]] ZUIKO21mmF2.jpg|[[/Olympus Zuiko 21 mm f/2/]] Olympus Zuiko 2,0 21mm.jpg|[[/Olympus Zuiko 21 mm f/2/]] IMG.svg|[[/Olympus Zuiko 21 mm f/3,5/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 24 mm f/2/]] (avant 1978) {{25}} Obiettivo fotografico ultragrandangolare, messa a fuoco elicoidale, con innesto a baionetta - Museo scienza tecnologia Milano 13087.jpg|Olympus Zuiko Auto-W 24 mm f/2,8 (1991) Zuiko shift 24mm.jpg|Olympus Zuiko shift 24 mm f/3,5 à décentrement Olympus Zuiko 24mm f 2.8.JPG|[[/Olympus Zuiko 24 mm f/2,8/]] (avant 1978) {{50}} IMG.svg|[[/Olympus Zuiko 28 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 3,5 28mm.jpg|[[/Olympus Zuiko 28 mm f/3,5/]] (avant 1978) {{25}} Olympus OM 2,8 35 Shift.jpg|Olympus Zuiko shift 35 mm f/2,8 à décentrement IMG.svg|[[/Olympus Zuiko 35 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 2,8 35mm.jpg|[[/Olympus Zuiko 35 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 35 mm f/3,5 Macro/]] (26&nbsp;septembre&nbsp;2005) {{75}} Olympus Zuiko MC-Macro 1-3,5 f=38mm lens (4243438710).jpg|[[/Olympus Zuiko MB 38 mm f/3,5 Macro/]] Olympus OM Zuiko Zoom 3570 mm f 4,0.jpg|[[/Olympus Zuiko Auto-zoom 35-70 mm f/4/]] (1982) OM Auto Zoom 3,6 f=35-70mm-19840912-RM-123616.jpg|[[/Olympus Zuiko MC Auto-zoom 35-70 mm f/3,6/]] Olympus OM Zuiko Zoom 35-105 f 3,5-4,5.jpg|[[/Olympus Zuiko Auto-zoom 35-105 mm f/3,5/4.5 close focus/]] IMG.svg|[[/Olympus Zuiko 50 mm f/1,2/]] (1982) {{50}} Olympus Zuiko 50mm f 1.4.JPG|[[/Olympus Zuiko 50 mm f/1,4/]] (avant 1978) {{25}} Olympus OM F.Zuiko 50 mm f 1,8.jpg|[[/Olympus Zuiko 50 mm f/1,8/]] Zuiko macro50F2.jpg|[[/Olympus Zuiko Auto-Macro 50 mm f/2/]] Olympus OM 3,5 50mm Makroobjektiv.jpg|[[/Olympus Zuiko Auto-macro 50 mm f/3,5]] IMG.svg|[[/Olympus Zuiko 55 mm f/1,2/]] IMG.svg|[[/Olympus Zuiko 85 mm f/2,0/]] IMG.svg|[[/Olympus Zuiko Zoom 65-200 mm f/4]] Olympus OM Zoom 4.0 75-150 mm.jpg|[[/Olympus Zuiko 75-150 mm f/4]] Zuiko macro 80mm.jpg|Olympus Zuiko macro 80 mm f/4 Olympus Zuiko 100mm f 2.8.JPG|[[/Olympus Zuiko 100 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 100 mm f/2/]] IMG.svg|[[/Olympus S Zuiko Zoom 100-200 mm f/5]] IMG.svg|[[/Olympus Zuiko 135 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 135 mm f/3,5/]] Olympus OM Zuiko Macro 135 mm f 4,5.jpg|[[/Olympus Zuiko macro 135 mm f/4,5/]] IMG.svg|[[/Olympus Zuiko 180 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 200 mm f/5/]] Olympus Zuiko 200mm f 4.JPG|[[/Olympus Zuiko 200 mm f/4/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 250 mm f/2/]] Olympus F.Zuiko 4.5 300 mm 06.jpg|[[Olympus F Zuiko 300 mm f/4,5]] Olympus F.Zuiko 4.5 300 mm 07.jpg|[[Olympus F Zuiko 300 mm f/4,5]] IMG.svg|[[/Olympus Zuiko 350 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 400 mm f/6,3/]] IMG.svg|[[/Olympus Zuiko 600 mm f/5,6/]] IMG.svg|[[/Olympus Zuiko Reflex 500 mm f/8/]] IMG.svg|[[/Olympus Zuiko 1000 mm f/11/]] </gallery> == Objectifs autofocus == === Séries Zuiko anciennes === <gallery> IMG.svg|Olympus Zuiko AF 24 mm f/2,8 IMG.svg|Olympus Zuiko AF 28 mm f/2,8 IMG.svg|Olympus Zuiko AF 50 mm f/2,8 Macro IMG.svg|Olympus Zuiko AF 50 mm f/1,8 IMG.svg|Olympus Lens AF 28-85 mm f/3.5/4.5 IMG.svg|[[/Olympus Lens AF 35-70 mm f/3.5/4.5/]] (1982) IMG.svg|Olympus Lens AF 35-105 mm f/3.5/4.5 IMG.svg|Olympus Lens AF 70-210 mm f/3.5/4.5 (1982) </gallery> === Série Zuiko Digital === <gallery> </gallery> === Série Four-Thirds === <gallery> Olympus four thirds camera.JPG Olympus four thirds lenses.JPG IMG.svg|[[/Olympus Zuiko Digital ED 7-14 mm f/4/]] (2005) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 9-18 mm f/4-5,6/]] (2008) {{75}} Olympus lens EZ1122.jpg|[[/Olympus Zuiko Digital 11-22 mm f/2,8-3,5|Olympus Zuiko Digital 11-22 mm f/2,8-3,5]] {{75}} Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD lens with Olympus Lens Hood LH-75B.jpg|[[/Olympus Zuiko Digital ED 12-60 mm f/2,8-4 SWD/]] (octobre 2007) {{100}} Olympus Zuiko Digital 14-42mm 3.5-5.6 ED (3795070609).jpg|[[/Olympus Zuiko Digital ED 14-42 mm f/3,5-5,6/]] (septembre 2006) {{100}} ZD 14 54 I DSC 5350.jpg|[[/Olympus Zuiko Digital 14-54 mm f/2,8-3,5/]] Olympus Zuiko Digital 14-45mm 3.5-5.6 (2179004620).jpg|[[/Olympus Zuiko Digital 14-45 mm f/3,5-5,6/]] Zuiko 14-35mm.jpg|[[/Olympus Zuiko Digital 14-35 f/2/]] Olympus Zuiko Digital 17.5-45mm 3.5-5.6 (2178211561).jpg|[[/Olympus Zuiko Digital 17,5-45 mm f/3,5-5,6/]] Olympus Zuiko Digital 25mm lens - front.jpg|[[/Olympus Zuiko Digital 25 mm f/2,8/ (Pancake)]] (mars 2008) {{100}} Olympus Zuiko Digital 35mm Macro 3.5 (2178211317).jpg|[[/Olympus Zuiko Digital 35 mm f/3,5 Macro/]] Objektiv Olympus ZUIKO DIGITAL 50mm Macro stehend.jpg|[[/Olympus Zuiko Digital ED 50 mm f/2 Macro/]] (juin&nbsp;2008) {{100}} Olympus Zuiko Digital 40-150mm f3.5-4.5 lens - front.jpg|[[/Olympus Zuiko Digital ED 40-150 mm f/3,5-4,5/]] (2006) {{100}} Olympus 50–200 2.8–3.5.jpg|50-200 mm f/2,8-3,5 ED Olympus E-500 + EC-20 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Zuiko Digital ED 50-200mm F2.8-3.5 SWD.jpg|50-200 mm f/2,8-3,5 ED Olympus E-330 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Objektiv Olympus ZUIKO DIGITAL 70-300mm by 300mm.jpg|[[/Olympus Zuiko Digital ED 70-300 mm f/4,0-5,6/]] (2007) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 90-250 mm f/2,8/]] (2007) {{25}} </gallery> === Série Micro Four-Thirds === <gallery> Objektiv Olympus M.ZUIKO DIGITAL 7-14mm stehend.jpg|Olympus M.Zuiko Digital 7-14 mm Objektiv Olympus M.ZUIKO DIGITAL 7-14mm.jpg MelvL P3180491 (5536855633).jpg|[[/Olympus M Zuiko Digital ED 9-18 mm f/4-5,6|Olympus M Zuiko Digital ED 9-18 mm f/4-5,6]] (avril 2010) {{50}} Olympus 9mm F8 bodycap lens on Air A01.jpg|Olympus 9 mm f/8 Olympus 9mm F8 bodycap lens on ep5.jpg Olympus 9mm F8 bodycap lens on GM5.jpg Olympus 9mm F8 Fisheye bodycap lens on E-P5.jpg IMG.svg|[[/Olympus M.Zuiko Digital ED 12 mm f/2|Olympus M.Zuiko Digital ED 12 mm f/2]] (30&nbsp;juin&nbsp;2011) {{75}} Olympus M.Zuiko Digital 14-42mm.png|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 L ED Olympus M.Zuiko Digital 14-42mm F3.5-5.6 cutted.jpg Olympus M.Zuiko digital 14-42mm f3.5-5.6 II R.jpg|[[/Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R]] M.Zuiko 12-50mm 02.jpg|[[/Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ|Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ]] IMG.svg|[[/Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II|Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II]] (5&nbsp;février&nbsp;2015) {{75}} Olympus Body Cap lens 15mm F8 n01.jpg|[[/Olympus Body Cap lens 15 mm f/8|Olympus Body Cap lens 15 mm f/8]] (septembre 2012) {{100}} 2016 0212 Olympus mft 25mm1.8.jpg|[[/Olympus M-Zuiko Digital 25 mm f/1,8/]] M.Zuiko 12-50mm 01.jpg|Olympus M.Zuiko Digital 12-50 mm Olympus M.Zuiko Digital 40-150mm.png|Olympus M.Zuiko Digital 40-150 mm Olympus E-M5 15.jpg|12-50 mm Olympus M.Zuiko Digital 40-150mm F2.8.jpg|[[/Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro|Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro]] (15&nbsp;septembre&nbsp;2014) {{100}} Olympus M Zuiko Digital ED 45mm F1.8.jpg|Olympus M Zuiko Digital ED 45 mm f/1,8 Olympus lens M.Zuiko 75 mm f1.8.jpg|[[/Olympus M.Zuiko Digital ED 75 mm f/1,8|Olympus M.Zuiko Digital ED 75 mm f/1,8]] (8&nbsp;février&nbsp;2012) {{100}} Olympus M.Zuiko Digital 300mm F4.0.jpg|Olympus M.Zuiko Digital 300 mm f/4,0 </gallery> === Objectifs spéciaux === <gallery> Image:24mmPCleft.jpg|PC 24 mm </gallery> == Compléments optiques == <gallery> File:Olympus TCON-14B.JPG|[[/Olympus TCON-14B|Olympus TCON-14B]] Complément optique télé destiné aux appareils E-10 et E-20 File:Olympus E-20 with TCON-14B.JPG|Olympus E-20 avec TCON-14B Fichier:OlympusTeleconv2x.png|Téléconvertisseur EC-20 2x File:Olympus EC-20.jpg|Téléconvertisseur EC-20 2x Image:IMG.svg|[[/Olympus EC14|Olympus EC14]] multiplicateur de focale 1,4x (2010) File:Olympus TCON-300S ohne Gegenlichtblende.JPG|Olympus TCON-300S File:Olympus E-20P mit TCON-300S.JPG|E-20P avec TCON-300S </gallery> == Flashes == <gallery> Olympus XA1 (2404583547).jpg|[[/Olympus A9M|Olympus A9M]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus A11|Olympus A11 monté sur Olympus XA4 Macro]] Olympus XA3 (2405412636).jpg|[[/Olympus A16|Olympus A16 monté sur Olympus XA1]] Blixt jm2.jpg|flash T32 pour OM-2 Blixt jm3.jpg|flash T32 pour OM-2 Blixt jm4.jpg|flash T32 pour OM-2 Blixt jm5.jpg|flash T32 pour OM-2 2006-07-07 00-35-52b.jpg Olympus FL-40.jpg|FL-40 Olympus FL-40 1.jpg|FL-40 Olympus FL-40 8.jpg Olympus FL-40 7.jpg Olympus FL-40 6.jpg Olympus FL-40 5.jpg Olympus FL-40 4.jpg Olympus Blitzgerät Auto Quick 310 38.jpg|flash Auto Quick 310 pour OM-2 </gallery> == Bagues-allonges == Elles sont utilisées pour la [[proxiphotographie]] et pour la [[macrophotographie]]. * '''Olympus EX-25''' : longueur 25 mm, monture 4/3 Olympus, 150 € <gallery> File:Olympus OM Zwischenringe 25 + 14mm.jpg|Bagues de 14 et 25 mm pour Olympus OM </gallery> == Accessoires divers == <gallery> MelvL P1260102 (5388033078).jpg|Viseur électronique VF-2 MelvL P1260105 (5387430951).jpg|Viseur électronique VF-2 MelvL P3180492 (5536856995).jpg|Pare-soleil LH-55B Adattatore per esposizioni manuali - Museo scienza tecnologia Milano 13097.jpg|Adaptateur pour l'exposition manuelle pour [[/Olympus OM10/]] Olympus OM Winder 2.jpg|OM winder 2 Olympus OM MD1 Motor.jpg|OM motor drive Olympus focusing screen 1-1 (5344261324).jpg|verre de visée pour Olympus OM-1 Olympus Winkelsucher OM.jpg|Viseur d'angle pour Olympus OM Olympus-OM-Macro-Flash-Shoe-Ring.JPG|support pour flash macro Olympus slide copier hg.jpg|duplicateur de diapositives Slide copier - Olympus bellows unit, modified to take a Pentax body.jpg Slide copier - Olympus bellows unit, modified to take a Pentax body - (1).jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 06.jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 08.jpg Olympus Kabelfernauslöser RM-UC1.jpg Olympus Li-ion Akkuladegerät BCM-2 21.jpg Carcasa y cámara de fotos subacuática.jpg|Caisson étanche PT-029 pour Olympus Stylus 600 (2001) </gallery> == Sacs et fourre-tout == La marque vend des étuis, sacs et fourre-tout adaptés à ses produits. {{Ph Fabricants}} lq1ft7nygvbauhcngamupg98acla7an 763855 763854 2026-04-17T09:16:05Z Banffy 34456 /* Série OM-System */ 763855 wikitext text/x-wiki {{Ph s Fabricants}} {{EnTravaux}} == À classer == <gallery> Olympus Superzoom 110 BW 1.JPG|Superzoom 110 BW Quick Flash AFL.jpg|Quick flash Olympus SZIII stereo microscope.jpg Olympus Stylus.jpg|Stylus Olympus mju ii.jpg|[[/Olympus Mju II/]] Olympus C-960 Zoom.jpg|C 960 Olympus Superzoom 120TC.jpg|Olympus </gallery> == Appareils 18 x 24 == <gallery> Olympus Pen img 0048.jpg|[[/Olympus Pen|Olympus Pen EE]] Olympus Pen img 1197.jpg|Olympus Pen Olympus pen camera.JPG|Olympus Pen Olympus Pen 6867.jpg Olympus Pen.jpg Olympus Pen 4397.jpg|[[/Olympus Pen révision 3|Olympus Pen révision 3]] (1959) Pen s 130503 019 (8705222446).jpg|[[/Olympus Pen S/]] (vers 1960) Image:IMG.svg|Olympus Pen EM Olympus Pen EE (type 1).jpg|[[/Olympus Pen EE|Olympus Pen EE (type 1)]] (vers 1968) {{25}} Olympus Pen EE-2 241-2599.jpg|[[/Olympus Pen EE-2|Olympus Pen EE-2]] Olympus pen ee3.jpg|[[/Olympus Pen EE-3|Olympus Pen EE-3]] Olympus Pen EE3.jpg|Olympus Pen EE-3 Olympus pen ees.jpg|[[/Olympus Pen EE S|Olympus Pen EE S]] Olympus PEN-EE S (meio quadro).jpg|Olympus Pen EE S Olympus Pen EES2.jpg|[[/Olympus Pen EES-2|Olympus Pen EES-2]] Olympus Pen EED.jpg|Olympus Pen EED Olympus pen eed.jpg|Olympus Pen EED 0607 Olympus EED with lens cap (9122191695).jpg|Olympus Pen EED 0606 Olympus EED no lens cap (9124412452).jpg|Olympus Pen EED MelvL P4290007 (5669556412).jpg|Olympus Pen EED MelvL P4290003 (5669548202).jpg|Olympus Pen EED MelvL P4290009 (5669557980).jpg|Olympus Pen EED MelvL P4290006 (5669403335).jpg|Olympus Pen EED MelvL P4290004 (5668984407).jpg|Olympus Pen EED MelvL P4290008 (5668985957).jpg|Olympus Pen EED MelvL P1040065 (5703691290).jpg|Olympus Pen EED MelvL (5703980674).jpg|Olympus Pen EED MelvL P1040116 (5709471065).jpg|Olympus Pen EED MelvL P1040153 (5730856146).jpg|Olympus Pen EED MelvL P1040155 (5726118704).jpg|Olympus Pen EED Olympus Pen EES-2 (6717094541).jpg|Olympus Pen EES-2 Pen D3.jpg|[[/Olympus Pen D3|Olympus Pen D3]] (1965-1969) Olympus PenF.jpg|[[/Olympus Pen F|Olympus Pen F]] (vers 1963) Olympus-Pen-FT-with-38mm1 8.jpg|[[/Olympus Pen FT|Olympus Pen FT]] (vers 1968) {{75}} </gallery> == Appareils 24 x 36 reflex == <gallery> Olympus FTL front.jpg|[[/Olympus FTL/]] (1971-1972) {{25}} Olympus OM-1 (13573573703).jpg|[[/Olympus OM-1/]] (1973-1974) {{25}} OM1NB 1.jpg|[[/Olympus OM-1N/]] Olympus OM1MD.jpg|[[/Olympus OM-1 MD/]] (avant 1979) {{50}} OM1-n MD (4072626146).jpg|[[/Olympus OM1-n MD/]] (1979-1983) Olympus OM-2 with Zuiko 50mm f1.8.jpg|[[/Olympus OM-2/]] (1976) {{75}} Olympus OM-2N img 0732.jpg|[[/Olympus OM-2N/]] {{25}} Olympus OM-2 SP.jpg|[[/Olympus OM-2 SP/]] {{25}} Olympus OM10 35-70mm.jpg|[[/Olympus OM10/]] (1978) {{100}} Olympusom3.jpg|[[/Olympus OM-3/]] {{25}} Olympus OM3 ti.jpg|OM-3 Ti OM-3Ti Black.jpg Olympus OM3 ti OM4 ti.jpg Olympus OM20 - Tokino 70-210.jpg|[[/Olympus OM20|Olympus OM20 = Olympus OMG (A)]] (1982) Olympus OM-30 (bottom).jpg|[[/Olympus OM30/]] (1982) OlympusOM4 1.JPG|[[/Olympus OM-4/]] {{50}} Olympus OM4 ti 01.jpg|OM-4 Ti Olympus OM-4 Ti.JPG|OM-4 Ti Olympus OM-4Ti worn black body with Zuiko 1.8-50mm lens and neckstrap.jpg Vintage Olympus OM-PC (aka OM-40) 35mm SLR Film Camera, Made In Japan, Circa 1985 (13517132323).jpg|[[/Olympus OM-40|Olympus OM-40 = OM-PC]] (vers 1985) </gallery> == Appareils 24 x 36 compacts == <gallery> Olympus LT1 (3007523325).jpg|[[/Olympus LT1/]] Olympus Superzoom 3000.jpg|Olympus Superzoom 3000 Olympus XA camera and film.jpg|[[/Olympus XA/]] {{25}} My Olympus XA1 (4379061989).jpg|[[/Olympus XA1/]] My Olympus XA2 (4989175842).jpg|[[/Olympus XA2/]] Olympus Ecru.jpg|Olympus Ecru Olympus Ecru (4766955124).jpg|[[/Olympus Ecru/]] série limitée du Mju Olympus Ecru cap.jpg Olympus Ecru back.jpg Olympus Ecru front.jpg Olympus Ecru 01.jpg Olympus Wide.jpg|Olympus wide Olympus Trip 35.jpeg|[[/Olympus Trip 35/]] (vers 1968) {{50}} Olympus-35 ECR.jpg|Olympus-35 ECR Olympus-35 SP.jpg|[[/Olympus 35 SP/]] (1968) {{25}} Olympus35DC3.jpg|35 DC Olympus35DC2.jpg|35 DC Olympus35DC1.jpg|35 DC My Olympus 35DC (4797809987).jpg|35 DC Olympus 35 RC img 1850.jpg|[[/Olympus 35 RC/]] (avant 1977) {{50}} Olympus 35RD.jpg|35 RD Olympus Stylus Epic 1118.jpg|[[/Olympus mju II|Olympus mju II = Olympus Stylus Epic]] {{25}} My Olympus XA-3 (4024574761).jpg|[[/Olympus XA3/]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus XA4 Macro/]] Olympus mju i.jpg|mju 1 Mju (3645746098).jpg 2009-11-26-Olympus-700BF-1.jpg|700 BF 2009-11-26-Olympus-700BF-2.jpg|700 BF 2009-11-26-Olympus-700BF-3.jpg|700 BF Olympus-stylus hg.jpg[Stylus zoom 115 Olympus Superzoom 120 1a.jpg|Superzoom 120 Olympus Superzoom 120TC.jpg|Olympus Superzoom 120TC My Olympus AF-1 Infinity (4876749434).jpg|[[/Olympus AF-1 Infinity/]] Olympus Infinity Jr. (4815671398).jpg|[[/Olympus Infinity Jr./]] Olympus AZ-200 Superzoom.jpg|Olympus AZ-200 Superzoom Olympus Trip MD3.jpg|Olympus Trip MD3 Olympus LT-105Z (6733278979).jpg|Olympus LT-105Z </gallery> == Appareils 24x36 bridge == <gallery> My Olympus IS-1 (4662576887).jpg|[[/Olympus IS-1|Olympus IS-1]] Olympus IS10 (3) (5789273975).jpg|[[/Olympus IS-10|Olympus IS-10]] {{25}} Olympus ED 35-180 (6175609523).jpg|[[/Olympus IS-3000/]] (1993) Olympus-IS-100-07.jpg|[[/Olympus IS-100|Olympus IS-100]] (1994) {{25}} Olympus IS100S (5) (5789275039).jpg|[[/Olympus IS-100S|Olympus IS-100S]] {{25}} Olympus Alvesgaspar.jpg|[[/Olympus IS-1000|Olympus IS-1000]] {{25}} </gallery> == Appareils pour le format AGFA Rapid == <gallery> Image:IMG.svg|[[/Olympus Pen RAPID EES|Olympus Pen RAPID EES]] Image:IMG.svg|[[/Olympus Pen RAPID EED|Olympus Pen RAPID EED]] </gallery> == Appareils pour le format 126 == <gallery> Olympus Quickmatic 600 (2759484117).jpg|[[/Olympus Quickmatic 600|Olympus Quickmatic 600]] </gallery> == Appareils pour le format APS == <gallery> Olympus i zoom 2000 (3854940049).jpg|[[/Olympus i zoom 2000/]] (2000) </gallery> == Appareils numériques non reflex == === année 1996 === <gallery> Image:IMG.svg|[[/Olympus D-200L|Olympus D-200L]] {{50}} (5&nbsp;septembre 1996) Image:IMG.svg|[[/Olympus D-300L|Olympus D-300L]] {{50}} (5&nbsp;septembre 1996) </gallery> === année 1997 === <gallery> Image:Olympus C-820L.jpg|[[/Olympus Camedia C-820L|Olympus Camedia C-820L]] {{50}} (septembre 1997) File:2009-11-26-Olympus-C-820L-1.jpg|C-820L File:2009-11-26-Olympus-C-820L-2.jpg|C-820L File:2009-11-26-Olympus-C-820L-3.jpg|C-820L File:2009-11-26-Olympus-C-820L-5.jpg|C-820L File:2009-11-26-Olympus-C-820L-6.jpg|C-820L File:2009-11-26-Olympus-C-820L-4.jpg|C-820L File:Camedia-C-820L-05.jpg|C-820L File:Camedia-C-820L-02.jpg|C-820L </gallery> === année 1998 === <gallery> Image:IMG.svg|[[/Olympus D-340L|Olympus D-340L]] {{50}} (28&nbsp;septembre 1998) File:Olympus C-900 ZOOM.jpg|[[/Olympus D-400|Olympus D-400 = Stylus Digital 400 = Olympus C900Z)]] {{75}} (2&nbsp;novembre&nbsp;1998) </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus D-340R|Olympus D-340R]] {{50}} (2&nbsp;janvier 1999) File:Olympus Camedia C-2000 Z.jpg|[[/Olympus C-2000 Zoom|Olympus C-2000 Zoom]] {{50}} (16&nbsp;février 1999) Image:IMG.svg|[[/Olympus C-21|Olympus C-21]] {{50}} (28&nbsp;juin&nbsp;1999) File:Olympus Camedia C-21T.commu CP+ 2011.jpg|Olympus Camedia C-21T.commu Image:IMG.svg|[[/Olympus D-450 Zoom|Olympus D-450 Zoom = Olympus C920Z]] {{50}} (31&nbsp;juillet 1999) Image:IMG.svg|[[/Olympus C-2020 Zoom|Olympus C-2020 Zoom]] (19&nbsp;octobre&nbsp;1999) </gallery> === année 2000 === <gallery> Olympos-Camedia-C3000.jpg|C3000 Image:IMG.svg|[[/Olympus C-3030 Zoom|Olympus C-3030 Zoom]] (27&nbsp;janvier&nbsp;2000) Image:IMG.svg|[[/Olympus D-360L|Olympus D-360L]] (2&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-460 Zoom|Olympus C-460 Zoom]] (8&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-3000 Zoom|Olympus C-3000 Zoom]] (24&nbsp;avril&nbsp;2000) Image:Olympus UZ-2100 03.jpg|[[/Olympus C-2100 Ultra Zoom|Olympus C-2100 Ultra Zoom]] (15&nbsp;juin&nbsp;2000) Image:Olympus UZ-2100 01.jpg Image:Olympus UZ-2100 02.jpg Image:IMG.svg|[[/Olympus D-490 Zoom|Olympus D-490 Zoom]] (1er&nbsp;août&nbsp;2000) File:Olympus E100RS.jpg|[[/Olympus E-100 RS|Olympus E-100 RS]] (22&nbsp;août&nbsp;2000) File:Olympus Camera E-100RS.jpg|E-100 RS Image:IMG.svg|[[/Olympus C-3040 Zoom|Olympus 3-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) Image:IMG.svg|[[/Olympus C-2040 Zoom|Olympus C-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) </gallery> === année 2001 === <gallery> Olympus Camedia C-1.jpg|[[/Olympus C-1|Olympus C-1]] (6&nbsp;mars&nbsp;2001) Olympus C-700 Ultra Zoom.jpg|[[/Olympus C-700 Ultra Zoom|Olympus C-700 Ultra Zoom]] (19&nbsp;mars&nbsp;2001) IMG.svg|[[/Olympus D-150 Zoom|Olympus D-150 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-510 Zoom|Olympus D-510 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-370|Olympus D-370]] (5&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus C-4040 Zoom|Olympus C-4040 Zoom]] (20&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus D-40 Zoom|Olympus D-40 Zoom]] (2&nbsp;septembre&nbsp;2001) Olympus Camedia C-2.jpg|[[/Olympus C-2|Olympus C-2]] (13&nbsp;septembre&nbsp;2001) Olympus Camedia C-3020.jpg|[[/Olympus C-3020 Zoom|Olympus C-3020 Zoom]] (15&nbsp;octobre&nbsp;2001) </gallery> === année 2002 === <gallery> File:My Olympus D-520Z (4794377895).jpg|[[/Olympus D-520 Zoom|Olympus D-520 Zoom]] (13&nbsp;mars&nbsp;2002) File:Olympus D-380.jpg|[[/Olympus D-380|Olympus D-380 = Olympus C-120]] (13&nbsp;mars&nbsp;2002) Olympus C-2020Z.jpg|Olympus Camedia C-2020Z OlympusC220ZoomCamera.jpg|C220Z File:Olympus Camedia C-720.jpg|[[/Olympus C-720 Ultra Zoom|Olympus C-720 Ultra Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-300 Zoom|Olympus C-300 Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-4000 Zoom|Olympus C-4000 Zoom]] (25&nbsp;juillet&nbsp;2002) File:Olympus C-5050Z, -Apr. 2007 a.jpg|[[/Olympus C-5050 Zoom|Olympus C-5050 Zoom]] (19&nbsp;août&nbsp;2002) File:Olympus C-5050Z, -6 Aug. 2006 a.jpg|C-5050 File:Olympus C-5050Z, -19 Nov. 2005 a.jpg|C-5050 Image:Olympus C-730UZ Front Left.jpg|[[/Olympus C-730 UZ|Olympus C-730 UZ]] (12&nbsp;septembre&nbsp;2002) Image:IMG.svg|[[/Olympus C-50 Zoom|Olympus C-50 Zoom]] (24&nbsp;septembre&nbsp;2002) </gallery> === année 2003 === <gallery> Image:IMG.svg|[[/Olympus Stylus 400|Olympus Stylus 400 =&nbsp;Olympus µ 400 Digital]] (9&nbsp;janvier&nbsp;2003) Image:IMG.svg|[[/Olympus Stylus 300|Olympus Stylus 300 =&nbsp;Olympus µ 300 Digital]] (9&nbsp;janvier&nbsp;2003) Fichier:Olympus Camedia C-740 Ultra Zoom 10.JPG|[[/Olympus C-740 Ultra Zoom|Olympus C-740 Ultra Zoom]] {{75}} (2&nbsp;mars&nbsp;2003) File:Olympus C-150.JPG|[[/Olympus Camedia C-150|Olympus Camedia C-150 =&nbsp;Olympus D-390]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -3.JPG|[[/Olympus D-560 Zoom|Olympus D-560 Zoom = Camedia C-350 zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -2.JPG Image:Olympus Camedia C-350 Zoom -1.JPG Image:Olympus Camedia C-350 Zoom.JPG Image:Olympus-C350Z.jpg Image:Olympus C-750.jpg|[[/Olympus C-750 Ultra Zoom|Olympus C-750 Ultra Zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus C-750 back.jpg Image:Olympus C-750 front right-1.jpg Image:Olympus C-750 front right.jpg Image:Olympus C-750 front left.jpg Image:Digital Camera.jpg|[[/Olympus C-5000 Zoom|Olympus C-5000 Zoom]] (29&nbsp;août&nbsp;2003) Olympus Camedia C-5000Z 3750.jpg Olympus Camedia C-5000Z 3751.jpg Olympus Camedia C-5000Z 3752.jpg Olympus Camedia C-5000Z 3753.jpg Olympus Camedia C-5000Z 3754.jpg Olympus Camedia C-5000Z 3755.jpg Olympus Camedia C-5000Z 3756.jpg Image:IMG.svg|[[/Olympus C-5060 Zoom|Olympus C-5060 Zoom]] (29&nbsp;septembre&nbsp;2003) </gallery> === année 2004 === <gallery> IMG.svg|[[/Olympus D-540 Zoom|Olympus D-540 Zoom]] (14&nbsp;février&nbsp;2004) IMG.svg|[[/Olympus D-580 Zoom|Olympus D-580 Zoom]] (14&nbsp;février&nbsp;2004) Stylus410specs.jpg|[[/Olympus Stylus 410|Olympus Stylus 410]] (14&nbsp;février&nbsp;2004) OLYMPUS C-8080WZ 01.jpg|[[/Olympus C-8080 WideZoom|Olympus C-8080 WideZoom]] (14&nbsp;février&nbsp;2004) Olympus CAMEDIA C-8080.JPG|C-8080 C-8080WZ rear.JPG|C-8080 C-8080WZ tele.JPG|C-8080 Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus C-765UZ, -13 juni 2006 a.jpg|[[/Olympus C-765 Ultra Zoom|Olympus C-765 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus C-766 UZ back.jpg Olympus C-765 UZ front.jpg IMG.svg|[[/Olympus C-770 Ultra Zoom|Olympus C-770 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus D-395.JPG|[[/Olympus D-395|Olympus D-395]] (18&nbsp;mars&nbsp;2004) Olympus C-60 Zoom.JPG|[[/Olympus C-60 Zoom|Olympus C-60 Zoom]] (18&nbsp;mars&nbsp;2004) Olympus µ-mini.jpeg|[[/Olympus Stylus Verve|Olympus Stylus Verve = Olympus Mju-mini = Olympus mju-ii]] (3&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus C-7000 Zoom|Olympus C-7000 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus D-535 Zoom|Olympus D-535 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus Stylus 500|Olympus Stylus 500]] (29&nbsp;novembre&nbsp;2004) </gallery> === année 2005 === <gallery> IMG.svg|[[/Olympus D-425/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-7070 Wide Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-5500 Sport Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus Stylus Verve S/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-545 Zoom/]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 3.JPG|[[/Olympus D-595 Zoom|Olympus D-595 Zoom = Olympus C-500Z]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 2.JPG Olympus C-500Z 1.JPG Olympus IR-300.jpg|[[/Olympus IR-300/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-630 Zoom/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus Stylus 800/]] (12&nbsp;mai&nbsp;2005) IMG.svg|[[/Olympus D-435/]] (20&nbsp;mai&nbsp;2005) Olympus FE 110 (2254131662).jpg|[[/Olympus FE-110/]] (29&nbsp;août&nbsp;2005) Olympus-SP-310-p1030353.jpg|[[/Olympus SP-310/]] {{00}} (29&nbsp;août&nbsp;2005) Olympus FE-120 01.jpg|[[/Olympus FE-120/]] {{50}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus Stylus 600/]] (29&nbsp;août&nbsp;2005) Oly SP-350-1.jpg|[[/Olympus SP-350/]] {{25}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-500 UZ/]] {{25}} (29&nbsp;août&nbsp;2005) Olympus FE-100 front.jpg|[[/Olympus FE-100/]] (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-700/]] (4&nbsp;octobre&nbsp;2005) </gallery> === année 2006 === <gallery> File:My Olympus SP-320 (4171943306).jpg|[[/Olympus SP-320|Olympus SP-320]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-115|Olympus FE-115]] (26&nbsp;janvier&nbsp;2006) {{25}} File:Olympus-digitale-camera-FE-130.JPG|[[/Olympus FE-130|Olympus FE-130]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-140|Olympus FE-140]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:IMG.svg|[[/Olympus FE-150|Olympus FE-150]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:Olympus µ 700.jpg|[[/Olympus Stylus 700|Olympus Stylus 700 = Mju 700 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 720 SW|Olympus Stylus 720 SW = Olympus Mju 720 SW Digital]] (26&nbsp;janvier&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 810|Olympus Stylus 810 = Olympus Mju 810 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:Olympus X760 01.jpg|[[/Olympus FE-170|Olympus FE-170 = Olympux X-760]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus FE-180|Olympus FE-180]] (24&nbsp;août&nbsp;2006) {{25}} Image:Olympus FE190.JPG|[[/Olympus FE-190|Olympus FE-190]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-200|Olympus FE-200]] (24&nbsp;août&nbsp;2006) {{50}} File:Olympus μ 725 SW.jpg|[[/Olympus Stylus 725 SW|Olympus Stylus 725 SW = Olympus Mju 725 SW Digital]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 730|Olympus Stylus 730 = Olympus Mju 730 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 740|Olympus Stylus 740 = Olympus Mju 740 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 750|Olympus Stylus 750 = Olympus Mju 750 Digital]] (24&nbsp;août&nbsp;2006) {{75}} Image:IMG.svg|[[/Olympus Stylus 1000|Olympus Stylus 1000 = Olympus Mju 1000 Digital]] (24&nbsp;août&nbsp;2006) {{75}} File:OlympusSP510UZ.jpg|[[/Olympus SP-510 UZ|Olympus SP-510 UZ]] (24&nbsp;août&nbsp;2006) {{50}} </gallery> === année 2007 === <gallery> Image:Olympus SP 550UZ.jpg|[[/Olympus SP-550 UZ|Olympus SP-550 UZ]] (25&nbsp;janvier&nbsp;2007) {{100}} File:Fe-210.png|[[/Olympus FE-210|Olympus FE-210 = X-775]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-230|Olympus FE-230]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-240|Olympus FE-240]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-250|Olympus FE-250]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Olympus µ 760.jpg|[[/Olympus Stylus 760|Olympus Stylus 760 = Olympus mju 760 Digital]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Stylus 770SW.jpg|[[/Olympus Stylus 770 SW|Olympus Stylus 770 SW = Olympus mju 770 SW Digital]] (25&nbsp;janvier&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 780|Olympus Stylus 780]] (5&nbsp;mars&nbsp;2007) Image:IMG.svg|[[/Olympus FE-270|Olympus FE-270]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-280|Olympus FE-280]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-290|Olympus FE-290]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-300|Olympus FE-300]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 790 SW|Olympus Stylus 790 SW = Olympus mju 790 SW Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 820|Olympus Stylus 820 = Olympus mju 820 Digital]] (23&nbsp;août&nbsp;2007) {{75}} File:OLYMPUS Mu 830.jpeg|[[/Olympus Stylus 830|Olympus Stylus 830 = Olympus mju 830 Digital]] (23&nbsp;août&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 1200|Olympus Stylus 1200 = Olympus mju 1200 Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus SP-560 UZ|Olympus SP-560 UZ]] (25&nbsp;janvier&nbsp;2007) {{75}} </gallery> === année 2008 === <gallery> File:Olympus SP-570UZ, -Nov. 2008 a.jpg|[[/Olympus SP-570 UZ|Olympus SP-570 UZ]] (2008) {{50}} Image:IMG.svg|[[/Olympus Mju 1040|Olympus Mju 1040]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1050sw|Olympus Mju 1050sw]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1060|Olympus Mju 1060]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-20|Olympus FE-20]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-360|Olympus FE-360]] (19&nbsp;août&nbsp;2008) {{75}} Image:IMG.svg|[[/Olympus FE-370|Olympus FE-370]] (2008) {{25}} </gallery> === année 2009 === <gallery> Bfishadow Olympus E-P1.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 bottom.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 top.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 back.jpg|Olympus Pen E-P1 Olympus Pen img 3486.jpg|Olympus Pen E-P1 Olympus IMG 2163.jpg|Olympus Pen E-P1 Olympus E-P1 (3634318402).jpg Olympus E-P1 (3634895524).jpg Olympus E-P1 (3634080929).jpg Olympus E-P1- Sleek frame (3634087049).jpg Olympus E-P1 (3634889300).jpg Olympus E-P1 (3634079289).jpg Olympus E-P1 (3633504211).jpg Olympus E-P1 (3634318984).jpg Olympus E-P1 (3634318918).jpg Olympus E-P1 (3633503717).jpg </gallery> === année 2010 === <gallery> File:Olympus PEN E-PL1.jpg|[[/Olympus PEN E-PL1|Olympus PEN E-PL1]] (10&nbsp;février&nbsp;2010) {{100}} </gallery> === année 2011 === <gallery> File:Olympus E-PL2 with Leica Summicron 50 f2 LTM lens.jpg|[[/Olympus Pen E-PL2|Olympus Pen E-PL2]] (6&nbsp;janvier&nbsp;2011) {{75}} Image:IMG.svg|[[/Olympus SP-610UZ|Olympus SP-610UZ]] (6&nbsp;janvier&nbsp;2011) {{75}} File:Olympus-XZ-1.jpg|[[/Olympus XZ-1|Olympus XZ-1]] (6&nbsp;janvier&nbsp;2011) {{100}} </gallery> === année 2012 === <gallery> Image:IMG.svg|[[/Olympus Tough TG-1 iHS|Olympus Tough TG-1 iHS]] (8 mai 2012) {{75}} </gallery> === année 2013 === <gallery> </gallery> === année 2014 === <gallery> </gallery> === année 2015 === <gallery> Olympus OM-D E-M5II (18421339075).jpg|[[/Olympus OM-D E-M5 II]] (5&nbsp;février&nbsp;2015) {{100}} IMG.svg|[[/Olympus Tough TG-4/]] (13&nbsp;avril&nbsp;2015) {{75}} Olympus OM-D E-M10 Mark II.JPG|[[/Olympus OM-D E-M10 II/]] (25&nbsp;août&nbsp;2015) {{75}} </gallery> === année 2016 === <gallery> Olympus PEN-F.jpg|[[/Olympus Pen-F/]] (27&nbsp;janvier&nbsp;2016) {{00}} </gallery> ==== à classer ==== <gallery> 2013-265-121 Tough New Toy (8700211111).jpg|Olympus Tough TG-2 OLYMPUS PEN MINI E-PM2 (8651180173).jpg|Olympus E-PM2 Olympus E-20P 01.jpg|Olympus E-20P Olympus E-20P 02.jpg Olympus E-20P 03.jpg Olympus E-20P 04.jpg Olympus E-20P 05.jpg Olympus E-20P 06.jpg Olympus E-20P 07.jpg Olympus E-20P 08.jpg Olympus E-20P 09.jpg Olympus E-20P 10.jpg Olympus E-20P 11.jpg Olympus X-750.jpg|Olympus X-750 Olympus PEN F.jpg|Olympus PEN F Olympus Pen F (digital).jpg|Olympus PEN F Olympus Pen F-IMG 9925.jpg|Olympus PEN F Olympus Pen F-IMG 9927.JPG|Olympus PEN F Olympus PEN F.jpg|Olympus PEN F Olympus PEN-F.jpg|Olympus PEN F Olympus PEN E-PL6 black kit lens 2016-03-03.jpg|Olympus PEN E-PL6 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15772393942).jpg|Olympus OM-D E-M1 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15770833985).jpg OM D E-M1 with 75mm f-1.8 (9869006524).jpg OM D E-M1 with 12-60mm f-2.8-4 ED SWD (9869086256).jpg Olympus OM-D E-M1 Mark II mock-up (rough model) 2017 CP+.jpg|Olympus OM-D E-M1 Mark II Olympus OM-D E-M1 Mark II mock-up (design model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II mock-up (body+power battery holder model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II magnesium-alloy chassis 2017 CP+.jpg Olympus OM-D E-M1 Mark II D81 8378-2.jpg Olympus.OM-D.E-M1.Mark.II.back.view.jpg Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus camedia C-800L.jpg|[[/Olympus Camedia C-800L/]] Olympus SH-1 zilver, -2015 a.jpg|Olympus SH-1 Olympus SH-1 zilver, -2015 b.jpg Olympus SH-1 zilver, -2015 c.jpg Olympus Pen EPL7.JPG|[[/Olympus Pen E-PL7/]] Digitalkamera von Olympus.JPG|FE-5020 Olympus XZ-10, -februari 2013 a.jpg|Olympus XZ-10 Leong IMG 2989 (6338669929).jpg Leong IMG 2986 (6339413622).jpg Leong IMG 0497 (6689370435).jpg P1000963 (6707919805).jpg Olympus OM-D E-M10 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 01.jpg Olympus OM-D E-M10 cutaway 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 cutted 1.jpg Olympus OM-D E-M10 cutted 2.jpg Olympus PEN E-P5 Back 1.jpg|Olympus PEN E-P5 Olympus PEN E-P5 Front 1.jpg Olympus PEN E-P5 Front 2.jpg EP5 with 45mm F1.8.jpg Olympus E-P5.jpg Olympus X-500 D-590Z C-470Z.jpg|X-500 = D-590Z = C-470Z Olympus E-PM1 + BCL-15.jpg|Olympus E-PM1 Olympus TG-820 Camera.jpg|Olympus TG-820 Olympus TG-820 front with lens cover open.jpg|Olympus TG-820 Olympus TG-820 Top.JPG|Olympus TG-820 Olympus E-P2.jpg|Olympus E-P2 Micro Four Thirds Olympus E-P2 with Panasonic Lumix G 20mm F1.7 ASPH aspherical pancake lens.jpg Olympus EPL5 top.jpg|E-PL5 Olympus EPL5 back 01.jpg|E-PL5 Olympus EPL5 back 02.jpg|E-PL5 Olympus EPL5 front lens.jpg|E-PL5 Olympus EPL5 front.jpg|E-PL5 Olympus epl5 vf4.jpg Olympus epl5 vf4 45mm 01.jpg Olympus epl5 vf4 45mm 02.jpg Olympus epl5 vf4 45mm 03.jpg Olympus PEN Lite and Nissin i40.jpg Olympus VR-340.JPG|Olympus VR-340 Olympus-digitale-camera-X-720.JPG|Olympus X-720 Olympus Camedia C-310 Zoom Digital Camera.JPG|Olympus Camedia C-310 Zoom Olympus PEN E-PL3.jpg|[[/Olympus Pen E-PL3|Olympus Pen E-PL3]] Olypmus SP-810UZ, closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_no_zoom,_flash_closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_full_zoom,_flash_opened.jpg|Olympus SP-810UZ Olympus E-P3 006.JPG|[[/Olympus E-P3/]] Olympus AZ-300 Superzoom.jpg|[[/Olympus AZ-300 Superzoom/]] Olympus SP560UZ DSCF9120.jpg|SP560 UZ Olympus u5000.jpg|Mju 5000 Stylus Tough 8000.jpg|Stylus Tough 8000 Olympus_SP590_UZ_01_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_02_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_03_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_04_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_05_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_06_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_07_(RaBoe).jpg|SP590 UZ Olympus SP590 UZ 2010-by-RaBoe-02.jpg Olympus SP590 UZ 2010-by-RaBoe-01.jpg FE-310.jpg|FE-310 Image:Olympus u850SW.jpg|mju 850SW Olympus µ850SW, -1 mei 2010 a.jpg Image:Olympus img 1845.jpg|C-1400 Image:Olympus_FE-340_8MP_camera_01.jpg|FE-340 Olympus-MicroFT-Model.jpg|Micro four thirds OlyE-P2Test10112009-01.jpg|EP-2 Olympus VR-310.jpg|Olympus VR-310 </gallery> == Appareils reflex numériques == === année 1997 === <gallery> Image:IMG.svg|[[/Olympus D-500L|Olympus D-500L]] {{50}} (10&nbsp;septembre 1997) Image:IMG.svg|[[/Olympus D-600L|Olympus D-600L]] {{50}} (10&nbsp;septembre 1997) Image:Olympus img 1846.jpg|C-1400 Image:Olympus img 1845.jpg|C-1400 </gallery> === année 1998 === <gallery> Image:Olympus C-1400 01.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 61.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 57.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus C-2500 L|Olympus C-2500 L]] {{50}} (18&nbsp;mars 1999) </gallery> === année 2000 === <gallery> File:Olympus E-10.jpg|[[/Olympus E-10|Olympus E-10]] (22&nbsp;août&nbsp;2000) File:Olympus E-20P.JPG|[[/Olympus E-20|Olympus E-20]] Olympus E-20n.jpg|Olympus E-20n </gallery> === année 2003 === <gallery> Image:Olympus E-1 2.jpg|[[/Olympus E-1|Olympus E-1]] (24&nbsp;juin&nbsp;2003) Image:E-1 hinten oben.jpg Image:E-1 Seite hinten.jpg Image:E-1 hinten.jpg File:E-1 vorne.jpg File:Olympus E-1 body.jpg </gallery> === année 2004 === <gallery> E-300.jpg|[[/Olympus E-300|Olympus E-300 (EVOLT E-300)]] (27&nbsp;septembre&nbsp;2004) </gallery> === année 2005 === <gallery> File:E-500 Body.jpg|[[/Olympus E-500|Olympus E-500]] {{25}} (2005) Olympus E-500 with Minolta MD Lens (5391265164).jpg|[[/Olympus E-500/]] (26&nbsp;septembre&nbsp;2005) </gallery> === année 2006 === <gallery> File:Olympus E-330. Zuiko Digital ED.jpg|[[/Olympus E-330|Olympus E-330 = EVOLT E-330]] {{25}} (26&nbsp;janvier&nbsp;2006) Image:Oly e 400 voorkant.jpg|[[/Olympus E-400|Olympus E-400]] {{75}} (14&nbsp;septembre&nbsp;2006) </gallery> === année 2007 === <gallery> Olympus E410 img 1030.jpg|[[/Olympus E-410/]] (5&nbsp;mars&nbsp;2007) Olympus E510 img 1029.jpg|[[/Olympus E-510/]] (5&nbsp;mars&nbsp;2007) P3069465 (3333310515).jpg|[[/Olympus E-3/]] Olympus_E-3_Camera.jpg|[[/Olympus E3/]] {{75}} (16&nbsp;octobre&nbsp;2007) </gallery> === année 2008 === <gallery> Olympus E-420.jpg|E-420 Olympus E-420 EZ40150.jpg|E-420 Olympus E-420 Body Front.jpg|E-420 Olympus E-420 Pancake25mm Top.jpg|E-420 Olympus E-420 Body Top XL.jpg|E-420 Image:Olymous E420 img 1248.jpg|E-420 Image:Olympus E-420 (back).jpg|E-420 Image:Olympus E-420 (front).jpg|E-420 </gallery> === année 2009 === <gallery> Olympus E-450.JPG|[[/Olympus E-450|Olympus E-450]] {{75}} (31&nbsp;mars&nbsp;2009) Olympus E-30 01.jpg|Olympus E-30 Olympus E-30 02.jpg|Olympus E-30 Olympus E-30 03.jpg|Olympus E-30 Olympus E-30 04.jpg|Olympus E-30 Olympus E-30 rear01.jpg|E30 Olympus E-30 front01.jpg|E30 E-30-back.jpg|E3 Olympus E-30 with ZD 14-54mm f2.8-3.5II 01.JPG|E30 Olympus E-30-Cutmodel.jpg|E30 coupé E-30-with-14-54.jpg|E30 Olympus E30-IMG 2445.jpg|E30 Olympus E30-IMG 2442.jpg|E30 Olympus E30-IMG 2441.jpg|E30 Olympus E-620 front.jpg|E-620 Olympus E-620.jpg|E-620 Olympus E-620 with battery grip.jpg|E-620 Olympus E-620 without lens.jpg|E620 Olympus E-620 swivel screen open.JPG|E620 Olympus E620 DSLR.jpg|E620 </gallery> === année 2010 === <gallery> Olympus-E5.jpg|E-5 </gallery> '''à classer''' <gallery> Olympus OM-D E-M1- 20131118.jpg|Olympus OM-D E-M1 OM-D EM-1.jpg|OM-D EM-1 Oly-EM1-connector.jpg Olympus OM-D E-M1 01.jpg Olympus OM-D E-M1 cutted 1.jpg Olympus OM-D E-M1 cutted 2.jpg Olympus OM-D E-M1 cutted 3.jpg Olympus OM-D E-M1 image stabilization unit.jpg Olympus E-M5 (front, cropped).jpg|OM-D E-M5 Oly-E-M5.jpg OLYMPUS OM-D E-M5.jpg Olympus OM-D E-M5.jpg Olympus E-M5, Nokton 25mm.jpg Olympus E-M5 01.jpg Olympus E-M5 02.jpg Olympus E-M5 03.jpg Olympus E-M5 04.jpg Olympus E-M5 05.jpg Olympus E-M5 06.jpg Olympus E-M5 08.jpg Olympus E-M5 07.jpg Olympus E-M5 09.jpg Olympus E-M5 10.jpg Olympus E-M5 11.jpg Olympus E-M5 12.jpg Olympus E-M5 13.jpg Olympus E-M5 14.jpg Olympus E-M5 15.jpg Olympus E-M5 16.jpg Olympus OM-D E-M5, Taipei, TW.jpg Olympus E-M5 + Bigma.jpg Micro Four Thirds Olympus OM-D E-M5 digital camera.jpg Oly-E-M5.jpg Olympus OM-D E-M5 Elite black kit.jpg Olympus OM-D E-M5 Elite black.jpg Olympus E-M5 with 45mm F1.8.jpg Olympus OM 50mm f1.8.jpg|E-420 Olympus-e-520-front.png|[[/Olympus E-520|Olympus E-520]] </gallery> == Modules pour smartphone == <gallery> Olympus Air A01,mounted lens and phone.jpg|Olympus Air A01 </gallery> == Objectifs à mise au point manuelle == === Série Pen === <gallery> Olympus-20mm-F3 5-with-TTL-No.jpg|[[/Olympus Zuiko 20 mm f/3,5/]] Image:PenF-Zuiko-20mm.JPG </gallery> === Série FTL (M42) === <gallery> M42.OffenblendOLYMPUS.jpg|Olympus M 42 Olympus FTL G.Zuiko 28 mm f 3,5.jpg|Olympus G.Zuiko 28 mm f/3,5 IMG.svg|Olympus Zuiko 35 mm f/2,8/ IMG.svg|Olympus Zuiko 50 mm f/1,4/ Olympus FTL F. Zuiko 50 mm f 1,8.jpg|Olympus Zuiko 50 mm f/1,8 IMG.svg|Olympus Zuiko 135 mm f/3,5/ IMG.svg|Olympus Zuiko 200 mm f/4,0/ </gallery> === Série OM-System === <gallery> OMLenses.jpg OM Zuiko f2 lenses.jpg|Zuiko f/2 </gallery> <gallery> IMG.svg|[[/Olympus Zuiko 8 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 16 mm f/3,5/]] (avant 1978) {{25}} Olympus Zuiko Auto-Macro 20mm 1-2 lens (4243439258).jpg|[[/Olympus Zuiko Auto-Macro 20 mm f/1,2/]] ZUIKO21mmF2.jpg|[[/Olympus Zuiko 21 mm f/2/]] Olympus Zuiko 2,0 21mm.jpg|[[/Olympus Zuiko 21 mm f/2/]] IMG.svg|[[/Olympus Zuiko 21 mm f/3,5/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 24 mm f/2/]] (avant 1978) {{25}} Obiettivo fotografico ultragrandangolare, messa a fuoco elicoidale, con innesto a baionetta - Museo scienza tecnologia Milano 13087.jpg|Olympus Zuiko Auto-W 24 mm f/2,8 (1991) Zuiko shift 24mm.jpg|Olympus Zuiko shift 24 mm f/3,5 à décentrement Olympus Zuiko 24mm f 2.8.JPG|[[/Olympus Zuiko 24 mm f/2,8/]] (avant 1978) {{50}} IMG.svg|[[/Olympus Zuiko 28 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 3,5 28mm.jpg|[[/Olympus Zuiko 28 mm f/3,5/]] (avant 1978) {{25}} Olympus OM 2,8 35 Shift.jpg|Olympus Zuiko shift 35 mm f/2,8 à décentrement IMG.svg|[[/Olympus Zuiko 35 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 2,8 35mm.jpg|[[/Olympus Zuiko 35 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 35 mm f/3,5 Macro/]] (26&nbsp;septembre&nbsp;2005) {{75}} Olympus Zuiko MC-Macro 1-3,5 f=38mm lens (4243438710).jpg|[[/Olympus Zuiko MB 38 mm f/3,5 Macro/]] Olympus OM Zuiko Zoom 3570 mm f 4,0.jpg|[[/Olympus Zuiko Auto-zoom 35-70 mm f/4/]] (1982) OM Auto Zoom 3,6 f=35-70mm-19840912-RM-123616.jpg|[[/Olympus Zuiko MC Auto-zoom 35-70 mm f/3,6/]] Olympus OM Zuiko Zoom 35-105 f 3,5-4,5.jpg|[[/Olympus Zuiko Auto-zoom 35-105 mm f/3,5/4.5 close focus/]] IMG.svg|[[/Olympus Zuiko 50 mm f/1,2/]] (1982) {{50}} Olympus Zuiko 50mm f 1.4.JPG|[[/Olympus Zuiko 50 mm f/1,4/]] (avant 1978) {{25}} Olympus OM F.Zuiko 50 mm f 1,8.jpg|[[/Olympus Zuiko 50 mm f/1,8/]] Zuiko macro50F2.jpg|[[/Olympus Zuiko Auto-Macro 50 mm f/2/]] Olympus OM 3,5 50mm Makroobjektiv.jpg|[[/Olympus Zuiko Auto-macro 50 mm f/3,5]] IMG.svg|[[/Olympus Zuiko 55 mm f/1,2/]] IMG.svg|[[/Olympus Zuiko 85 mm f/2,0/]] IMG.svg|[[/Olympus Zuiko Zoom 65-200 mm f/4/]] Olympus OM Zoom 4.0 75-150 mm.jpg|[[/Olympus Zuiko 75-150 mm f/4/]] Zuiko macro 80mm.jpg|Olympus Zuiko macro 80 mm f/4 Olympus Zuiko 100mm f 2.8.JPG|[[/Olympus Zuiko 100 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 100 mm f/2/]] IMG.svg|[[/Olympus S Zuiko Zoom 100-200 mm f/5]] IMG.svg|[[/Olympus Zuiko 135 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 135 mm f/3,5/]] Olympus OM Zuiko Macro 135 mm f 4,5.jpg|[[/Olympus Zuiko macro 135 mm f/4,5/]] IMG.svg|[[/Olympus Zuiko 180 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 200 mm f/5/]] Olympus Zuiko 200mm f 4.JPG|[[/Olympus Zuiko 200 mm f/4/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 250 mm f/2/]] Olympus F.Zuiko 4.5 300 mm 06.jpg|[[Olympus F Zuiko 300 mm f/4,5]] Olympus F.Zuiko 4.5 300 mm 07.jpg|[[Olympus F Zuiko 300 mm f/4,5]] IMG.svg|[[/Olympus Zuiko 350 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 400 mm f/6,3/]] IMG.svg|[[/Olympus Zuiko 600 mm f/5,6/]] IMG.svg|[[/Olympus Zuiko Reflex 500 mm f/8/]] IMG.svg|[[/Olympus Zuiko 1000 mm f/11/]] </gallery> == Objectifs autofocus == === Séries Zuiko anciennes === <gallery> IMG.svg|Olympus Zuiko AF 24 mm f/2,8 IMG.svg|Olympus Zuiko AF 28 mm f/2,8 IMG.svg|Olympus Zuiko AF 50 mm f/2,8 Macro IMG.svg|Olympus Zuiko AF 50 mm f/1,8 IMG.svg|Olympus Lens AF 28-85 mm f/3.5/4.5 IMG.svg|[[/Olympus Lens AF 35-70 mm f/3.5/4.5/]] (1982) IMG.svg|Olympus Lens AF 35-105 mm f/3.5/4.5 IMG.svg|Olympus Lens AF 70-210 mm f/3.5/4.5 (1982) </gallery> === Série Zuiko Digital === <gallery> </gallery> === Série Four-Thirds === <gallery> Olympus four thirds camera.JPG Olympus four thirds lenses.JPG IMG.svg|[[/Olympus Zuiko Digital ED 7-14 mm f/4/]] (2005) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 9-18 mm f/4-5,6/]] (2008) {{75}} Olympus lens EZ1122.jpg|[[/Olympus Zuiko Digital 11-22 mm f/2,8-3,5|Olympus Zuiko Digital 11-22 mm f/2,8-3,5]] {{75}} Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD lens with Olympus Lens Hood LH-75B.jpg|[[/Olympus Zuiko Digital ED 12-60 mm f/2,8-4 SWD/]] (octobre 2007) {{100}} Olympus Zuiko Digital 14-42mm 3.5-5.6 ED (3795070609).jpg|[[/Olympus Zuiko Digital ED 14-42 mm f/3,5-5,6/]] (septembre 2006) {{100}} ZD 14 54 I DSC 5350.jpg|[[/Olympus Zuiko Digital 14-54 mm f/2,8-3,5/]] Olympus Zuiko Digital 14-45mm 3.5-5.6 (2179004620).jpg|[[/Olympus Zuiko Digital 14-45 mm f/3,5-5,6/]] Zuiko 14-35mm.jpg|[[/Olympus Zuiko Digital 14-35 f/2/]] Olympus Zuiko Digital 17.5-45mm 3.5-5.6 (2178211561).jpg|[[/Olympus Zuiko Digital 17,5-45 mm f/3,5-5,6/]] Olympus Zuiko Digital 25mm lens - front.jpg|[[/Olympus Zuiko Digital 25 mm f/2,8/ (Pancake)]] (mars 2008) {{100}} Olympus Zuiko Digital 35mm Macro 3.5 (2178211317).jpg|[[/Olympus Zuiko Digital 35 mm f/3,5 Macro/]] Objektiv Olympus ZUIKO DIGITAL 50mm Macro stehend.jpg|[[/Olympus Zuiko Digital ED 50 mm f/2 Macro/]] (juin&nbsp;2008) {{100}} Olympus Zuiko Digital 40-150mm f3.5-4.5 lens - front.jpg|[[/Olympus Zuiko Digital ED 40-150 mm f/3,5-4,5/]] (2006) {{100}} Olympus 50–200 2.8–3.5.jpg|50-200 mm f/2,8-3,5 ED Olympus E-500 + EC-20 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Zuiko Digital ED 50-200mm F2.8-3.5 SWD.jpg|50-200 mm f/2,8-3,5 ED Olympus E-330 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Objektiv Olympus ZUIKO DIGITAL 70-300mm by 300mm.jpg|[[/Olympus Zuiko Digital ED 70-300 mm f/4,0-5,6/]] (2007) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 90-250 mm f/2,8/]] (2007) {{25}} </gallery> === Série Micro Four-Thirds === <gallery> Objektiv Olympus M.ZUIKO DIGITAL 7-14mm stehend.jpg|Olympus M.Zuiko Digital 7-14 mm Objektiv Olympus M.ZUIKO DIGITAL 7-14mm.jpg MelvL P3180491 (5536855633).jpg|[[/Olympus M Zuiko Digital ED 9-18 mm f/4-5,6|Olympus M Zuiko Digital ED 9-18 mm f/4-5,6]] (avril 2010) {{50}} Olympus 9mm F8 bodycap lens on Air A01.jpg|Olympus 9 mm f/8 Olympus 9mm F8 bodycap lens on ep5.jpg Olympus 9mm F8 bodycap lens on GM5.jpg Olympus 9mm F8 Fisheye bodycap lens on E-P5.jpg IMG.svg|[[/Olympus M.Zuiko Digital ED 12 mm f/2|Olympus M.Zuiko Digital ED 12 mm f/2]] (30&nbsp;juin&nbsp;2011) {{75}} Olympus M.Zuiko Digital 14-42mm.png|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 L ED Olympus M.Zuiko Digital 14-42mm F3.5-5.6 cutted.jpg Olympus M.Zuiko digital 14-42mm f3.5-5.6 II R.jpg|[[/Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R]] M.Zuiko 12-50mm 02.jpg|[[/Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ|Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ]] IMG.svg|[[/Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II|Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II]] (5&nbsp;février&nbsp;2015) {{75}} Olympus Body Cap lens 15mm F8 n01.jpg|[[/Olympus Body Cap lens 15 mm f/8|Olympus Body Cap lens 15 mm f/8]] (septembre 2012) {{100}} 2016 0212 Olympus mft 25mm1.8.jpg|[[/Olympus M-Zuiko Digital 25 mm f/1,8/]] M.Zuiko 12-50mm 01.jpg|Olympus M.Zuiko Digital 12-50 mm Olympus M.Zuiko Digital 40-150mm.png|Olympus M.Zuiko Digital 40-150 mm Olympus E-M5 15.jpg|12-50 mm Olympus M.Zuiko Digital 40-150mm F2.8.jpg|[[/Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro|Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro]] (15&nbsp;septembre&nbsp;2014) {{100}} Olympus M Zuiko Digital ED 45mm F1.8.jpg|Olympus M Zuiko Digital ED 45 mm f/1,8 Olympus lens M.Zuiko 75 mm f1.8.jpg|[[/Olympus M.Zuiko Digital ED 75 mm f/1,8|Olympus M.Zuiko Digital ED 75 mm f/1,8]] (8&nbsp;février&nbsp;2012) {{100}} Olympus M.Zuiko Digital 300mm F4.0.jpg|Olympus M.Zuiko Digital 300 mm f/4,0 </gallery> === Objectifs spéciaux === <gallery> Image:24mmPCleft.jpg|PC 24 mm </gallery> == Compléments optiques == <gallery> File:Olympus TCON-14B.JPG|[[/Olympus TCON-14B|Olympus TCON-14B]] Complément optique télé destiné aux appareils E-10 et E-20 File:Olympus E-20 with TCON-14B.JPG|Olympus E-20 avec TCON-14B Fichier:OlympusTeleconv2x.png|Téléconvertisseur EC-20 2x File:Olympus EC-20.jpg|Téléconvertisseur EC-20 2x Image:IMG.svg|[[/Olympus EC14|Olympus EC14]] multiplicateur de focale 1,4x (2010) File:Olympus TCON-300S ohne Gegenlichtblende.JPG|Olympus TCON-300S File:Olympus E-20P mit TCON-300S.JPG|E-20P avec TCON-300S </gallery> == Flashes == <gallery> Olympus XA1 (2404583547).jpg|[[/Olympus A9M|Olympus A9M]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus A11|Olympus A11 monté sur Olympus XA4 Macro]] Olympus XA3 (2405412636).jpg|[[/Olympus A16|Olympus A16 monté sur Olympus XA1]] Blixt jm2.jpg|flash T32 pour OM-2 Blixt jm3.jpg|flash T32 pour OM-2 Blixt jm4.jpg|flash T32 pour OM-2 Blixt jm5.jpg|flash T32 pour OM-2 2006-07-07 00-35-52b.jpg Olympus FL-40.jpg|FL-40 Olympus FL-40 1.jpg|FL-40 Olympus FL-40 8.jpg Olympus FL-40 7.jpg Olympus FL-40 6.jpg Olympus FL-40 5.jpg Olympus FL-40 4.jpg Olympus Blitzgerät Auto Quick 310 38.jpg|flash Auto Quick 310 pour OM-2 </gallery> == Bagues-allonges == Elles sont utilisées pour la [[proxiphotographie]] et pour la [[macrophotographie]]. * '''Olympus EX-25''' : longueur 25 mm, monture 4/3 Olympus, 150 € <gallery> File:Olympus OM Zwischenringe 25 + 14mm.jpg|Bagues de 14 et 25 mm pour Olympus OM </gallery> == Accessoires divers == <gallery> MelvL P1260102 (5388033078).jpg|Viseur électronique VF-2 MelvL P1260105 (5387430951).jpg|Viseur électronique VF-2 MelvL P3180492 (5536856995).jpg|Pare-soleil LH-55B Adattatore per esposizioni manuali - Museo scienza tecnologia Milano 13097.jpg|Adaptateur pour l'exposition manuelle pour [[/Olympus OM10/]] Olympus OM Winder 2.jpg|OM winder 2 Olympus OM MD1 Motor.jpg|OM motor drive Olympus focusing screen 1-1 (5344261324).jpg|verre de visée pour Olympus OM-1 Olympus Winkelsucher OM.jpg|Viseur d'angle pour Olympus OM Olympus-OM-Macro-Flash-Shoe-Ring.JPG|support pour flash macro Olympus slide copier hg.jpg|duplicateur de diapositives Slide copier - Olympus bellows unit, modified to take a Pentax body.jpg Slide copier - Olympus bellows unit, modified to take a Pentax body - (1).jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 06.jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 08.jpg Olympus Kabelfernauslöser RM-UC1.jpg Olympus Li-ion Akkuladegerät BCM-2 21.jpg Carcasa y cámara de fotos subacuática.jpg|Caisson étanche PT-029 pour Olympus Stylus 600 (2001) </gallery> == Sacs et fourre-tout == La marque vend des étuis, sacs et fourre-tout adaptés à ses produits. {{Ph Fabricants}} la5tvfp6djs9d9o98prvp091hh6q0ew 763856 763855 2026-04-17T09:31:10Z Banffy 34456 /* Série OM-System */ 763856 wikitext text/x-wiki {{Ph s Fabricants}} {{EnTravaux}} == À classer == <gallery> Olympus Superzoom 110 BW 1.JPG|Superzoom 110 BW Quick Flash AFL.jpg|Quick flash Olympus SZIII stereo microscope.jpg Olympus Stylus.jpg|Stylus Olympus mju ii.jpg|[[/Olympus Mju II/]] Olympus C-960 Zoom.jpg|C 960 Olympus Superzoom 120TC.jpg|Olympus </gallery> == Appareils 18 x 24 == <gallery> Olympus Pen img 0048.jpg|[[/Olympus Pen|Olympus Pen EE]] Olympus Pen img 1197.jpg|Olympus Pen Olympus pen camera.JPG|Olympus Pen Olympus Pen 6867.jpg Olympus Pen.jpg Olympus Pen 4397.jpg|[[/Olympus Pen révision 3|Olympus Pen révision 3]] (1959) Pen s 130503 019 (8705222446).jpg|[[/Olympus Pen S/]] (vers 1960) Image:IMG.svg|Olympus Pen EM Olympus Pen EE (type 1).jpg|[[/Olympus Pen EE|Olympus Pen EE (type 1)]] (vers 1968) {{25}} Olympus Pen EE-2 241-2599.jpg|[[/Olympus Pen EE-2|Olympus Pen EE-2]] Olympus pen ee3.jpg|[[/Olympus Pen EE-3|Olympus Pen EE-3]] Olympus Pen EE3.jpg|Olympus Pen EE-3 Olympus pen ees.jpg|[[/Olympus Pen EE S|Olympus Pen EE S]] Olympus PEN-EE S (meio quadro).jpg|Olympus Pen EE S Olympus Pen EES2.jpg|[[/Olympus Pen EES-2|Olympus Pen EES-2]] Olympus Pen EED.jpg|Olympus Pen EED Olympus pen eed.jpg|Olympus Pen EED 0607 Olympus EED with lens cap (9122191695).jpg|Olympus Pen EED 0606 Olympus EED no lens cap (9124412452).jpg|Olympus Pen EED MelvL P4290007 (5669556412).jpg|Olympus Pen EED MelvL P4290003 (5669548202).jpg|Olympus Pen EED MelvL P4290009 (5669557980).jpg|Olympus Pen EED MelvL P4290006 (5669403335).jpg|Olympus Pen EED MelvL P4290004 (5668984407).jpg|Olympus Pen EED MelvL P4290008 (5668985957).jpg|Olympus Pen EED MelvL P1040065 (5703691290).jpg|Olympus Pen EED MelvL (5703980674).jpg|Olympus Pen EED MelvL P1040116 (5709471065).jpg|Olympus Pen EED MelvL P1040153 (5730856146).jpg|Olympus Pen EED MelvL P1040155 (5726118704).jpg|Olympus Pen EED Olympus Pen EES-2 (6717094541).jpg|Olympus Pen EES-2 Pen D3.jpg|[[/Olympus Pen D3|Olympus Pen D3]] (1965-1969) Olympus PenF.jpg|[[/Olympus Pen F|Olympus Pen F]] (vers 1963) Olympus-Pen-FT-with-38mm1 8.jpg|[[/Olympus Pen FT|Olympus Pen FT]] (vers 1968) {{75}} </gallery> == Appareils 24 x 36 reflex == <gallery> Olympus FTL front.jpg|[[/Olympus FTL/]] (1971-1972) {{25}} Olympus OM-1 (13573573703).jpg|[[/Olympus OM-1/]] (1973-1974) {{25}} OM1NB 1.jpg|[[/Olympus OM-1N/]] Olympus OM1MD.jpg|[[/Olympus OM-1 MD/]] (avant 1979) {{50}} OM1-n MD (4072626146).jpg|[[/Olympus OM1-n MD/]] (1979-1983) Olympus OM-2 with Zuiko 50mm f1.8.jpg|[[/Olympus OM-2/]] (1976) {{75}} Olympus OM-2N img 0732.jpg|[[/Olympus OM-2N/]] {{25}} Olympus OM-2 SP.jpg|[[/Olympus OM-2 SP/]] {{25}} Olympus OM10 35-70mm.jpg|[[/Olympus OM10/]] (1978) {{100}} Olympusom3.jpg|[[/Olympus OM-3/]] {{25}} Olympus OM3 ti.jpg|OM-3 Ti OM-3Ti Black.jpg Olympus OM3 ti OM4 ti.jpg Olympus OM20 - Tokino 70-210.jpg|[[/Olympus OM20|Olympus OM20 = Olympus OMG (A)]] (1982) Olympus OM-30 (bottom).jpg|[[/Olympus OM30/]] (1982) OlympusOM4 1.JPG|[[/Olympus OM-4/]] {{50}} Olympus OM4 ti 01.jpg|OM-4 Ti Olympus OM-4 Ti.JPG|OM-4 Ti Olympus OM-4Ti worn black body with Zuiko 1.8-50mm lens and neckstrap.jpg Vintage Olympus OM-PC (aka OM-40) 35mm SLR Film Camera, Made In Japan, Circa 1985 (13517132323).jpg|[[/Olympus OM-40|Olympus OM-40 = OM-PC]] (vers 1985) </gallery> == Appareils 24 x 36 compacts == <gallery> Olympus LT1 (3007523325).jpg|[[/Olympus LT1/]] Olympus Superzoom 3000.jpg|Olympus Superzoom 3000 Olympus XA camera and film.jpg|[[/Olympus XA/]] {{25}} My Olympus XA1 (4379061989).jpg|[[/Olympus XA1/]] My Olympus XA2 (4989175842).jpg|[[/Olympus XA2/]] Olympus Ecru.jpg|Olympus Ecru Olympus Ecru (4766955124).jpg|[[/Olympus Ecru/]] série limitée du Mju Olympus Ecru cap.jpg Olympus Ecru back.jpg Olympus Ecru front.jpg Olympus Ecru 01.jpg Olympus Wide.jpg|Olympus wide Olympus Trip 35.jpeg|[[/Olympus Trip 35/]] (vers 1968) {{50}} Olympus-35 ECR.jpg|Olympus-35 ECR Olympus-35 SP.jpg|[[/Olympus 35 SP/]] (1968) {{25}} Olympus35DC3.jpg|35 DC Olympus35DC2.jpg|35 DC Olympus35DC1.jpg|35 DC My Olympus 35DC (4797809987).jpg|35 DC Olympus 35 RC img 1850.jpg|[[/Olympus 35 RC/]] (avant 1977) {{50}} Olympus 35RD.jpg|35 RD Olympus Stylus Epic 1118.jpg|[[/Olympus mju II|Olympus mju II = Olympus Stylus Epic]] {{25}} My Olympus XA-3 (4024574761).jpg|[[/Olympus XA3/]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus XA4 Macro/]] Olympus mju i.jpg|mju 1 Mju (3645746098).jpg 2009-11-26-Olympus-700BF-1.jpg|700 BF 2009-11-26-Olympus-700BF-2.jpg|700 BF 2009-11-26-Olympus-700BF-3.jpg|700 BF Olympus-stylus hg.jpg[Stylus zoom 115 Olympus Superzoom 120 1a.jpg|Superzoom 120 Olympus Superzoom 120TC.jpg|Olympus Superzoom 120TC My Olympus AF-1 Infinity (4876749434).jpg|[[/Olympus AF-1 Infinity/]] Olympus Infinity Jr. (4815671398).jpg|[[/Olympus Infinity Jr./]] Olympus AZ-200 Superzoom.jpg|Olympus AZ-200 Superzoom Olympus Trip MD3.jpg|Olympus Trip MD3 Olympus LT-105Z (6733278979).jpg|Olympus LT-105Z </gallery> == Appareils 24x36 bridge == <gallery> My Olympus IS-1 (4662576887).jpg|[[/Olympus IS-1|Olympus IS-1]] Olympus IS10 (3) (5789273975).jpg|[[/Olympus IS-10|Olympus IS-10]] {{25}} Olympus ED 35-180 (6175609523).jpg|[[/Olympus IS-3000/]] (1993) Olympus-IS-100-07.jpg|[[/Olympus IS-100|Olympus IS-100]] (1994) {{25}} Olympus IS100S (5) (5789275039).jpg|[[/Olympus IS-100S|Olympus IS-100S]] {{25}} Olympus Alvesgaspar.jpg|[[/Olympus IS-1000|Olympus IS-1000]] {{25}} </gallery> == Appareils pour le format AGFA Rapid == <gallery> Image:IMG.svg|[[/Olympus Pen RAPID EES|Olympus Pen RAPID EES]] Image:IMG.svg|[[/Olympus Pen RAPID EED|Olympus Pen RAPID EED]] </gallery> == Appareils pour le format 126 == <gallery> Olympus Quickmatic 600 (2759484117).jpg|[[/Olympus Quickmatic 600|Olympus Quickmatic 600]] </gallery> == Appareils pour le format APS == <gallery> Olympus i zoom 2000 (3854940049).jpg|[[/Olympus i zoom 2000/]] (2000) </gallery> == Appareils numériques non reflex == === année 1996 === <gallery> Image:IMG.svg|[[/Olympus D-200L|Olympus D-200L]] {{50}} (5&nbsp;septembre 1996) Image:IMG.svg|[[/Olympus D-300L|Olympus D-300L]] {{50}} (5&nbsp;septembre 1996) </gallery> === année 1997 === <gallery> Image:Olympus C-820L.jpg|[[/Olympus Camedia C-820L|Olympus Camedia C-820L]] {{50}} (septembre 1997) File:2009-11-26-Olympus-C-820L-1.jpg|C-820L File:2009-11-26-Olympus-C-820L-2.jpg|C-820L File:2009-11-26-Olympus-C-820L-3.jpg|C-820L File:2009-11-26-Olympus-C-820L-5.jpg|C-820L File:2009-11-26-Olympus-C-820L-6.jpg|C-820L File:2009-11-26-Olympus-C-820L-4.jpg|C-820L File:Camedia-C-820L-05.jpg|C-820L File:Camedia-C-820L-02.jpg|C-820L </gallery> === année 1998 === <gallery> Image:IMG.svg|[[/Olympus D-340L|Olympus D-340L]] {{50}} (28&nbsp;septembre 1998) File:Olympus C-900 ZOOM.jpg|[[/Olympus D-400|Olympus D-400 = Stylus Digital 400 = Olympus C900Z)]] {{75}} (2&nbsp;novembre&nbsp;1998) </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus D-340R|Olympus D-340R]] {{50}} (2&nbsp;janvier 1999) File:Olympus Camedia C-2000 Z.jpg|[[/Olympus C-2000 Zoom|Olympus C-2000 Zoom]] {{50}} (16&nbsp;février 1999) Image:IMG.svg|[[/Olympus C-21|Olympus C-21]] {{50}} (28&nbsp;juin&nbsp;1999) File:Olympus Camedia C-21T.commu CP+ 2011.jpg|Olympus Camedia C-21T.commu Image:IMG.svg|[[/Olympus D-450 Zoom|Olympus D-450 Zoom = Olympus C920Z]] {{50}} (31&nbsp;juillet 1999) Image:IMG.svg|[[/Olympus C-2020 Zoom|Olympus C-2020 Zoom]] (19&nbsp;octobre&nbsp;1999) </gallery> === année 2000 === <gallery> Olympos-Camedia-C3000.jpg|C3000 Image:IMG.svg|[[/Olympus C-3030 Zoom|Olympus C-3030 Zoom]] (27&nbsp;janvier&nbsp;2000) Image:IMG.svg|[[/Olympus D-360L|Olympus D-360L]] (2&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-460 Zoom|Olympus C-460 Zoom]] (8&nbsp;février&nbsp;2000) Image:IMG.svg|[[/Olympus C-3000 Zoom|Olympus C-3000 Zoom]] (24&nbsp;avril&nbsp;2000) Image:Olympus UZ-2100 03.jpg|[[/Olympus C-2100 Ultra Zoom|Olympus C-2100 Ultra Zoom]] (15&nbsp;juin&nbsp;2000) Image:Olympus UZ-2100 01.jpg Image:Olympus UZ-2100 02.jpg Image:IMG.svg|[[/Olympus D-490 Zoom|Olympus D-490 Zoom]] (1er&nbsp;août&nbsp;2000) File:Olympus E100RS.jpg|[[/Olympus E-100 RS|Olympus E-100 RS]] (22&nbsp;août&nbsp;2000) File:Olympus Camera E-100RS.jpg|E-100 RS Image:IMG.svg|[[/Olympus C-3040 Zoom|Olympus 3-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) Image:IMG.svg|[[/Olympus C-2040 Zoom|Olympus C-2040 Zoom]] (21&nbsp;novembre&nbsp;2000) </gallery> === année 2001 === <gallery> Olympus Camedia C-1.jpg|[[/Olympus C-1|Olympus C-1]] (6&nbsp;mars&nbsp;2001) Olympus C-700 Ultra Zoom.jpg|[[/Olympus C-700 Ultra Zoom|Olympus C-700 Ultra Zoom]] (19&nbsp;mars&nbsp;2001) IMG.svg|[[/Olympus D-150 Zoom|Olympus D-150 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-510 Zoom|Olympus D-510 Zoom]] (8&nbsp;mai&nbsp;2001) IMG.svg|[[/Olympus D-370|Olympus D-370]] (5&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus C-4040 Zoom|Olympus C-4040 Zoom]] (20&nbsp;juin&nbsp;2001) IMG.svg|[[/Olympus D-40 Zoom|Olympus D-40 Zoom]] (2&nbsp;septembre&nbsp;2001) Olympus Camedia C-2.jpg|[[/Olympus C-2|Olympus C-2]] (13&nbsp;septembre&nbsp;2001) Olympus Camedia C-3020.jpg|[[/Olympus C-3020 Zoom|Olympus C-3020 Zoom]] (15&nbsp;octobre&nbsp;2001) </gallery> === année 2002 === <gallery> File:My Olympus D-520Z (4794377895).jpg|[[/Olympus D-520 Zoom|Olympus D-520 Zoom]] (13&nbsp;mars&nbsp;2002) File:Olympus D-380.jpg|[[/Olympus D-380|Olympus D-380 = Olympus C-120]] (13&nbsp;mars&nbsp;2002) Olympus C-2020Z.jpg|Olympus Camedia C-2020Z OlympusC220ZoomCamera.jpg|C220Z File:Olympus Camedia C-720.jpg|[[/Olympus C-720 Ultra Zoom|Olympus C-720 Ultra Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-300 Zoom|Olympus C-300 Zoom]] (8&nbsp;mai&nbsp;2002) Image:IMG.svg|[[/Olympus C-4000 Zoom|Olympus C-4000 Zoom]] (25&nbsp;juillet&nbsp;2002) File:Olympus C-5050Z, -Apr. 2007 a.jpg|[[/Olympus C-5050 Zoom|Olympus C-5050 Zoom]] (19&nbsp;août&nbsp;2002) File:Olympus C-5050Z, -6 Aug. 2006 a.jpg|C-5050 File:Olympus C-5050Z, -19 Nov. 2005 a.jpg|C-5050 Image:Olympus C-730UZ Front Left.jpg|[[/Olympus C-730 UZ|Olympus C-730 UZ]] (12&nbsp;septembre&nbsp;2002) Image:IMG.svg|[[/Olympus C-50 Zoom|Olympus C-50 Zoom]] (24&nbsp;septembre&nbsp;2002) </gallery> === année 2003 === <gallery> Image:IMG.svg|[[/Olympus Stylus 400|Olympus Stylus 400 =&nbsp;Olympus µ 400 Digital]] (9&nbsp;janvier&nbsp;2003) Image:IMG.svg|[[/Olympus Stylus 300|Olympus Stylus 300 =&nbsp;Olympus µ 300 Digital]] (9&nbsp;janvier&nbsp;2003) Fichier:Olympus Camedia C-740 Ultra Zoom 10.JPG|[[/Olympus C-740 Ultra Zoom|Olympus C-740 Ultra Zoom]] {{75}} (2&nbsp;mars&nbsp;2003) File:Olympus C-150.JPG|[[/Olympus Camedia C-150|Olympus Camedia C-150 =&nbsp;Olympus D-390]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -3.JPG|[[/Olympus D-560 Zoom|Olympus D-560 Zoom = Camedia C-350 zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus Camedia C-350 Zoom -2.JPG Image:Olympus Camedia C-350 Zoom -1.JPG Image:Olympus Camedia C-350 Zoom.JPG Image:Olympus-C350Z.jpg Image:Olympus C-750.jpg|[[/Olympus C-750 Ultra Zoom|Olympus C-750 Ultra Zoom]] (2&nbsp;mars&nbsp;2003) Image:Olympus C-750 back.jpg Image:Olympus C-750 front right-1.jpg Image:Olympus C-750 front right.jpg Image:Olympus C-750 front left.jpg Image:Digital Camera.jpg|[[/Olympus C-5000 Zoom|Olympus C-5000 Zoom]] (29&nbsp;août&nbsp;2003) Olympus Camedia C-5000Z 3750.jpg Olympus Camedia C-5000Z 3751.jpg Olympus Camedia C-5000Z 3752.jpg Olympus Camedia C-5000Z 3753.jpg Olympus Camedia C-5000Z 3754.jpg Olympus Camedia C-5000Z 3755.jpg Olympus Camedia C-5000Z 3756.jpg Image:IMG.svg|[[/Olympus C-5060 Zoom|Olympus C-5060 Zoom]] (29&nbsp;septembre&nbsp;2003) </gallery> === année 2004 === <gallery> IMG.svg|[[/Olympus D-540 Zoom|Olympus D-540 Zoom]] (14&nbsp;février&nbsp;2004) IMG.svg|[[/Olympus D-580 Zoom|Olympus D-580 Zoom]] (14&nbsp;février&nbsp;2004) Stylus410specs.jpg|[[/Olympus Stylus 410|Olympus Stylus 410]] (14&nbsp;février&nbsp;2004) OLYMPUS C-8080WZ 01.jpg|[[/Olympus C-8080 WideZoom|Olympus C-8080 WideZoom]] (14&nbsp;février&nbsp;2004) Olympus CAMEDIA C-8080.JPG|C-8080 C-8080WZ rear.JPG|C-8080 C-8080WZ tele.JPG|C-8080 Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus C-765UZ, -13 juni 2006 a.jpg|[[/Olympus C-765 Ultra Zoom|Olympus C-765 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus C-766 UZ back.jpg Olympus C-765 UZ front.jpg IMG.svg|[[/Olympus C-770 Ultra Zoom|Olympus C-770 Ultra Zoom]] (14&nbsp;février&nbsp;2004) Olympus D-395.JPG|[[/Olympus D-395|Olympus D-395]] (18&nbsp;mars&nbsp;2004) Olympus C-60 Zoom.JPG|[[/Olympus C-60 Zoom|Olympus C-60 Zoom]] (18&nbsp;mars&nbsp;2004) Olympus µ-mini.jpeg|[[/Olympus Stylus Verve|Olympus Stylus Verve = Olympus Mju-mini = Olympus mju-ii]] (3&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus C-7000 Zoom|Olympus C-7000 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus D-535 Zoom|Olympus D-535 Zoom]] (16&nbsp;septembre&nbsp;2004) IMG.svg|[[/Olympus Stylus 500|Olympus Stylus 500]] (29&nbsp;novembre&nbsp;2004) </gallery> === année 2005 === <gallery> IMG.svg|[[/Olympus D-425/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-7070 Wide Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus C-5500 Sport Zoom/]] (5&nbsp;janvier&nbsp;2005) IMG.svg|[[/Olympus Stylus Verve S/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-545 Zoom/]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 3.JPG|[[/Olympus D-595 Zoom|Olympus D-595 Zoom = Olympus C-500Z]] (17&nbsp;février&nbsp;2005) Olympus C-500Z 2.JPG Olympus C-500Z 1.JPG Olympus IR-300.jpg|[[/Olympus IR-300/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus D-630 Zoom/]] (17&nbsp;février&nbsp;2005) IMG.svg|[[/Olympus Stylus 800/]] (12&nbsp;mai&nbsp;2005) IMG.svg|[[/Olympus D-435/]] (20&nbsp;mai&nbsp;2005) Olympus FE 110 (2254131662).jpg|[[/Olympus FE-110/]] (29&nbsp;août&nbsp;2005) Olympus-SP-310-p1030353.jpg|[[/Olympus SP-310/]] {{00}} (29&nbsp;août&nbsp;2005) Olympus FE-120 01.jpg|[[/Olympus FE-120/]] {{50}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus Stylus 600/]] (29&nbsp;août&nbsp;2005) Oly SP-350-1.jpg|[[/Olympus SP-350/]] {{25}} (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-500 UZ/]] {{25}} (29&nbsp;août&nbsp;2005) Olympus FE-100 front.jpg|[[/Olympus FE-100/]] (29&nbsp;août&nbsp;2005) IMG.svg|[[/Olympus SP-700/]] (4&nbsp;octobre&nbsp;2005) </gallery> === année 2006 === <gallery> File:My Olympus SP-320 (4171943306).jpg|[[/Olympus SP-320|Olympus SP-320]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-115|Olympus FE-115]] (26&nbsp;janvier&nbsp;2006) {{25}} File:Olympus-digitale-camera-FE-130.JPG|[[/Olympus FE-130|Olympus FE-130]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-140|Olympus FE-140]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:IMG.svg|[[/Olympus FE-150|Olympus FE-150]] (26&nbsp;janvier&nbsp;2006) {{25}} Image:Olympus µ 700.jpg|[[/Olympus Stylus 700|Olympus Stylus 700 = Mju 700 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 720 SW|Olympus Stylus 720 SW = Olympus Mju 720 SW Digital]] (26&nbsp;janvier&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 810|Olympus Stylus 810 = Olympus Mju 810 Digital]] (26&nbsp;janvier&nbsp;2006) {{50}} Image:Olympus X760 01.jpg|[[/Olympus FE-170|Olympus FE-170 = Olympux X-760]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus FE-180|Olympus FE-180]] (24&nbsp;août&nbsp;2006) {{25}} Image:Olympus FE190.JPG|[[/Olympus FE-190|Olympus FE-190]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus FE-200|Olympus FE-200]] (24&nbsp;août&nbsp;2006) {{50}} File:Olympus μ 725 SW.jpg|[[/Olympus Stylus 725 SW|Olympus Stylus 725 SW = Olympus Mju 725 SW Digital]] (24&nbsp;août&nbsp;2006) Image:IMG.svg|[[/Olympus Stylus 730|Olympus Stylus 730 = Olympus Mju 730 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 740|Olympus Stylus 740 = Olympus Mju 740 Digital]] (24&nbsp;août&nbsp;2006) {{50}} Image:IMG.svg|[[/Olympus Stylus 750|Olympus Stylus 750 = Olympus Mju 750 Digital]] (24&nbsp;août&nbsp;2006) {{75}} Image:IMG.svg|[[/Olympus Stylus 1000|Olympus Stylus 1000 = Olympus Mju 1000 Digital]] (24&nbsp;août&nbsp;2006) {{75}} File:OlympusSP510UZ.jpg|[[/Olympus SP-510 UZ|Olympus SP-510 UZ]] (24&nbsp;août&nbsp;2006) {{50}} </gallery> === année 2007 === <gallery> Image:Olympus SP 550UZ.jpg|[[/Olympus SP-550 UZ|Olympus SP-550 UZ]] (25&nbsp;janvier&nbsp;2007) {{100}} File:Fe-210.png|[[/Olympus FE-210|Olympus FE-210 = X-775]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-230|Olympus FE-230]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-240|Olympus FE-240]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-250|Olympus FE-250]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Olympus µ 760.jpg|[[/Olympus Stylus 760|Olympus Stylus 760 = Olympus mju 760 Digital]] (25&nbsp;janvier&nbsp;2007) {{75}} Image:Stylus 770SW.jpg|[[/Olympus Stylus 770 SW|Olympus Stylus 770 SW = Olympus mju 770 SW Digital]] (25&nbsp;janvier&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 780|Olympus Stylus 780]] (5&nbsp;mars&nbsp;2007) Image:IMG.svg|[[/Olympus FE-270|Olympus FE-270]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-280|Olympus FE-280]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-290|Olympus FE-290]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus FE-300|Olympus FE-300]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 790 SW|Olympus Stylus 790 SW = Olympus mju 790 SW Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus Stylus 820|Olympus Stylus 820 = Olympus mju 820 Digital]] (23&nbsp;août&nbsp;2007) {{75}} File:OLYMPUS Mu 830.jpeg|[[/Olympus Stylus 830|Olympus Stylus 830 = Olympus mju 830 Digital]] (23&nbsp;août&nbsp;2007) {{100}} Image:IMG.svg|[[/Olympus Stylus 1200|Olympus Stylus 1200 = Olympus mju 1200 Digital]] (23&nbsp;août&nbsp;2007) {{75}} Image:IMG.svg|[[/Olympus SP-560 UZ|Olympus SP-560 UZ]] (25&nbsp;janvier&nbsp;2007) {{75}} </gallery> === année 2008 === <gallery> File:Olympus SP-570UZ, -Nov. 2008 a.jpg|[[/Olympus SP-570 UZ|Olympus SP-570 UZ]] (2008) {{50}} Image:IMG.svg|[[/Olympus Mju 1040|Olympus Mju 1040]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1050sw|Olympus Mju 1050sw]] (2008) {{25}} Image:IMG.svg|[[/Olympus Mju 1060|Olympus Mju 1060]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-20|Olympus FE-20]] (2008) {{25}} Image:IMG.svg|[[/Olympus FE-360|Olympus FE-360]] (19&nbsp;août&nbsp;2008) {{75}} Image:IMG.svg|[[/Olympus FE-370|Olympus FE-370]] (2008) {{25}} </gallery> === année 2009 === <gallery> Bfishadow Olympus E-P1.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 bottom.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 top.jpg|Olympus Pen E-P1 Bfishadow Olympus E-P1 back.jpg|Olympus Pen E-P1 Olympus Pen img 3486.jpg|Olympus Pen E-P1 Olympus IMG 2163.jpg|Olympus Pen E-P1 Olympus E-P1 (3634318402).jpg Olympus E-P1 (3634895524).jpg Olympus E-P1 (3634080929).jpg Olympus E-P1- Sleek frame (3634087049).jpg Olympus E-P1 (3634889300).jpg Olympus E-P1 (3634079289).jpg Olympus E-P1 (3633504211).jpg Olympus E-P1 (3634318984).jpg Olympus E-P1 (3634318918).jpg Olympus E-P1 (3633503717).jpg </gallery> === année 2010 === <gallery> File:Olympus PEN E-PL1.jpg|[[/Olympus PEN E-PL1|Olympus PEN E-PL1]] (10&nbsp;février&nbsp;2010) {{100}} </gallery> === année 2011 === <gallery> File:Olympus E-PL2 with Leica Summicron 50 f2 LTM lens.jpg|[[/Olympus Pen E-PL2|Olympus Pen E-PL2]] (6&nbsp;janvier&nbsp;2011) {{75}} Image:IMG.svg|[[/Olympus SP-610UZ|Olympus SP-610UZ]] (6&nbsp;janvier&nbsp;2011) {{75}} File:Olympus-XZ-1.jpg|[[/Olympus XZ-1|Olympus XZ-1]] (6&nbsp;janvier&nbsp;2011) {{100}} </gallery> === année 2012 === <gallery> Image:IMG.svg|[[/Olympus Tough TG-1 iHS|Olympus Tough TG-1 iHS]] (8 mai 2012) {{75}} </gallery> === année 2013 === <gallery> </gallery> === année 2014 === <gallery> </gallery> === année 2015 === <gallery> Olympus OM-D E-M5II (18421339075).jpg|[[/Olympus OM-D E-M5 II]] (5&nbsp;février&nbsp;2015) {{100}} IMG.svg|[[/Olympus Tough TG-4/]] (13&nbsp;avril&nbsp;2015) {{75}} Olympus OM-D E-M10 Mark II.JPG|[[/Olympus OM-D E-M10 II/]] (25&nbsp;août&nbsp;2015) {{75}} </gallery> === année 2016 === <gallery> Olympus PEN-F.jpg|[[/Olympus Pen-F/]] (27&nbsp;janvier&nbsp;2016) {{00}} </gallery> ==== à classer ==== <gallery> 2013-265-121 Tough New Toy (8700211111).jpg|Olympus Tough TG-2 OLYMPUS PEN MINI E-PM2 (8651180173).jpg|Olympus E-PM2 Olympus E-20P 01.jpg|Olympus E-20P Olympus E-20P 02.jpg Olympus E-20P 03.jpg Olympus E-20P 04.jpg Olympus E-20P 05.jpg Olympus E-20P 06.jpg Olympus E-20P 07.jpg Olympus E-20P 08.jpg Olympus E-20P 09.jpg Olympus E-20P 10.jpg Olympus E-20P 11.jpg Olympus X-750.jpg|Olympus X-750 Olympus PEN F.jpg|Olympus PEN F Olympus Pen F (digital).jpg|Olympus PEN F Olympus Pen F-IMG 9925.jpg|Olympus PEN F Olympus Pen F-IMG 9927.JPG|Olympus PEN F Olympus PEN F.jpg|Olympus PEN F Olympus PEN-F.jpg|Olympus PEN F Olympus PEN E-PL6 black kit lens 2016-03-03.jpg|Olympus PEN E-PL6 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15772393942).jpg|Olympus OM-D E-M1 Olympus OM D E-M1 - Johnragai Gear - 09.11.2014 (15770833985).jpg OM D E-M1 with 75mm f-1.8 (9869006524).jpg OM D E-M1 with 12-60mm f-2.8-4 ED SWD (9869086256).jpg Olympus OM-D E-M1 Mark II mock-up (rough model) 2017 CP+.jpg|Olympus OM-D E-M1 Mark II Olympus OM-D E-M1 Mark II mock-up (design model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II mock-up (body+power battery holder model) 2017 CP+.jpg Olympus OM-D E-M1 Mark II magnesium-alloy chassis 2017 CP+.jpg Olympus OM-D E-M1 Mark II D81 8378-2.jpg Olympus.OM-D.E-M1.Mark.II.back.view.jpg Olympus C-760 UltraZoom (2178205925).jpg|[[/Olympus Camedia C-760 UZ/]] Olympus camedia C-800L.jpg|[[/Olympus Camedia C-800L/]] Olympus SH-1 zilver, -2015 a.jpg|Olympus SH-1 Olympus SH-1 zilver, -2015 b.jpg Olympus SH-1 zilver, -2015 c.jpg Olympus Pen EPL7.JPG|[[/Olympus Pen E-PL7/]] Digitalkamera von Olympus.JPG|FE-5020 Olympus XZ-10, -februari 2013 a.jpg|Olympus XZ-10 Leong IMG 2989 (6338669929).jpg Leong IMG 2986 (6339413622).jpg Leong IMG 0497 (6689370435).jpg P1000963 (6707919805).jpg Olympus OM-D E-M10 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 01.jpg Olympus OM-D E-M10 cutaway 2014 CP+.jpg|Olympus OM-D E-M10 Olympus OM-D E-M10 cutted 1.jpg Olympus OM-D E-M10 cutted 2.jpg Olympus PEN E-P5 Back 1.jpg|Olympus PEN E-P5 Olympus PEN E-P5 Front 1.jpg Olympus PEN E-P5 Front 2.jpg EP5 with 45mm F1.8.jpg Olympus E-P5.jpg Olympus X-500 D-590Z C-470Z.jpg|X-500 = D-590Z = C-470Z Olympus E-PM1 + BCL-15.jpg|Olympus E-PM1 Olympus TG-820 Camera.jpg|Olympus TG-820 Olympus TG-820 front with lens cover open.jpg|Olympus TG-820 Olympus TG-820 Top.JPG|Olympus TG-820 Olympus E-P2.jpg|Olympus E-P2 Micro Four Thirds Olympus E-P2 with Panasonic Lumix G 20mm F1.7 ASPH aspherical pancake lens.jpg Olympus EPL5 top.jpg|E-PL5 Olympus EPL5 back 01.jpg|E-PL5 Olympus EPL5 back 02.jpg|E-PL5 Olympus EPL5 front lens.jpg|E-PL5 Olympus EPL5 front.jpg|E-PL5 Olympus epl5 vf4.jpg Olympus epl5 vf4 45mm 01.jpg Olympus epl5 vf4 45mm 02.jpg Olympus epl5 vf4 45mm 03.jpg Olympus PEN Lite and Nissin i40.jpg Olympus VR-340.JPG|Olympus VR-340 Olympus-digitale-camera-X-720.JPG|Olympus X-720 Olympus Camedia C-310 Zoom Digital Camera.JPG|Olympus Camedia C-310 Zoom Olympus PEN E-PL3.jpg|[[/Olympus Pen E-PL3|Olympus Pen E-PL3]] Olypmus SP-810UZ, closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_no_zoom,_flash_closed.jpg|Olympus SP-810UZ Olympus_SP-810UZ,_full_zoom,_flash_opened.jpg|Olympus SP-810UZ Olympus E-P3 006.JPG|[[/Olympus E-P3/]] Olympus AZ-300 Superzoom.jpg|[[/Olympus AZ-300 Superzoom/]] Olympus SP560UZ DSCF9120.jpg|SP560 UZ Olympus u5000.jpg|Mju 5000 Stylus Tough 8000.jpg|Stylus Tough 8000 Olympus_SP590_UZ_01_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_02_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_03_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_04_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_05_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_06_(RaBoe).jpg|SP590 UZ Olympus_SP590_UZ_07_(RaBoe).jpg|SP590 UZ Olympus SP590 UZ 2010-by-RaBoe-02.jpg Olympus SP590 UZ 2010-by-RaBoe-01.jpg FE-310.jpg|FE-310 Image:Olympus u850SW.jpg|mju 850SW Olympus µ850SW, -1 mei 2010 a.jpg Image:Olympus img 1845.jpg|C-1400 Image:Olympus_FE-340_8MP_camera_01.jpg|FE-340 Olympus-MicroFT-Model.jpg|Micro four thirds OlyE-P2Test10112009-01.jpg|EP-2 Olympus VR-310.jpg|Olympus VR-310 </gallery> == Appareils reflex numériques == === année 1997 === <gallery> Image:IMG.svg|[[/Olympus D-500L|Olympus D-500L]] {{50}} (10&nbsp;septembre 1997) Image:IMG.svg|[[/Olympus D-600L|Olympus D-600L]] {{50}} (10&nbsp;septembre 1997) Image:Olympus img 1846.jpg|C-1400 Image:Olympus img 1845.jpg|C-1400 </gallery> === année 1998 === <gallery> Image:Olympus C-1400 01.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 61.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} Image:Olympus Camedia C 1400 XL 57.jpg|[[/Olympus D-620L|Olympus D-620L = Olympus C1400XL]] (2&nbsp;novembre 1998) {{75}} </gallery> === année 1999 === <gallery> Image:IMG.svg|[[/Olympus C-2500 L|Olympus C-2500 L]] {{50}} (18&nbsp;mars 1999) </gallery> === année 2000 === <gallery> File:Olympus E-10.jpg|[[/Olympus E-10|Olympus E-10]] (22&nbsp;août&nbsp;2000) File:Olympus E-20P.JPG|[[/Olympus E-20|Olympus E-20]] Olympus E-20n.jpg|Olympus E-20n </gallery> === année 2003 === <gallery> Image:Olympus E-1 2.jpg|[[/Olympus E-1|Olympus E-1]] (24&nbsp;juin&nbsp;2003) Image:E-1 hinten oben.jpg Image:E-1 Seite hinten.jpg Image:E-1 hinten.jpg File:E-1 vorne.jpg File:Olympus E-1 body.jpg </gallery> === année 2004 === <gallery> E-300.jpg|[[/Olympus E-300|Olympus E-300 (EVOLT E-300)]] (27&nbsp;septembre&nbsp;2004) </gallery> === année 2005 === <gallery> File:E-500 Body.jpg|[[/Olympus E-500|Olympus E-500]] {{25}} (2005) Olympus E-500 with Minolta MD Lens (5391265164).jpg|[[/Olympus E-500/]] (26&nbsp;septembre&nbsp;2005) </gallery> === année 2006 === <gallery> File:Olympus E-330. Zuiko Digital ED.jpg|[[/Olympus E-330|Olympus E-330 = EVOLT E-330]] {{25}} (26&nbsp;janvier&nbsp;2006) Image:Oly e 400 voorkant.jpg|[[/Olympus E-400|Olympus E-400]] {{75}} (14&nbsp;septembre&nbsp;2006) </gallery> === année 2007 === <gallery> Olympus E410 img 1030.jpg|[[/Olympus E-410/]] (5&nbsp;mars&nbsp;2007) Olympus E510 img 1029.jpg|[[/Olympus E-510/]] (5&nbsp;mars&nbsp;2007) P3069465 (3333310515).jpg|[[/Olympus E-3/]] Olympus_E-3_Camera.jpg|[[/Olympus E3/]] {{75}} (16&nbsp;octobre&nbsp;2007) </gallery> === année 2008 === <gallery> Olympus E-420.jpg|E-420 Olympus E-420 EZ40150.jpg|E-420 Olympus E-420 Body Front.jpg|E-420 Olympus E-420 Pancake25mm Top.jpg|E-420 Olympus E-420 Body Top XL.jpg|E-420 Image:Olymous E420 img 1248.jpg|E-420 Image:Olympus E-420 (back).jpg|E-420 Image:Olympus E-420 (front).jpg|E-420 </gallery> === année 2009 === <gallery> Olympus E-450.JPG|[[/Olympus E-450|Olympus E-450]] {{75}} (31&nbsp;mars&nbsp;2009) Olympus E-30 01.jpg|Olympus E-30 Olympus E-30 02.jpg|Olympus E-30 Olympus E-30 03.jpg|Olympus E-30 Olympus E-30 04.jpg|Olympus E-30 Olympus E-30 rear01.jpg|E30 Olympus E-30 front01.jpg|E30 E-30-back.jpg|E3 Olympus E-30 with ZD 14-54mm f2.8-3.5II 01.JPG|E30 Olympus E-30-Cutmodel.jpg|E30 coupé E-30-with-14-54.jpg|E30 Olympus E30-IMG 2445.jpg|E30 Olympus E30-IMG 2442.jpg|E30 Olympus E30-IMG 2441.jpg|E30 Olympus E-620 front.jpg|E-620 Olympus E-620.jpg|E-620 Olympus E-620 with battery grip.jpg|E-620 Olympus E-620 without lens.jpg|E620 Olympus E-620 swivel screen open.JPG|E620 Olympus E620 DSLR.jpg|E620 </gallery> === année 2010 === <gallery> Olympus-E5.jpg|E-5 </gallery> '''à classer''' <gallery> Olympus OM-D E-M1- 20131118.jpg|Olympus OM-D E-M1 OM-D EM-1.jpg|OM-D EM-1 Oly-EM1-connector.jpg Olympus OM-D E-M1 01.jpg Olympus OM-D E-M1 cutted 1.jpg Olympus OM-D E-M1 cutted 2.jpg Olympus OM-D E-M1 cutted 3.jpg Olympus OM-D E-M1 image stabilization unit.jpg Olympus E-M5 (front, cropped).jpg|OM-D E-M5 Oly-E-M5.jpg OLYMPUS OM-D E-M5.jpg Olympus OM-D E-M5.jpg Olympus E-M5, Nokton 25mm.jpg Olympus E-M5 01.jpg Olympus E-M5 02.jpg Olympus E-M5 03.jpg Olympus E-M5 04.jpg Olympus E-M5 05.jpg Olympus E-M5 06.jpg Olympus E-M5 08.jpg Olympus E-M5 07.jpg Olympus E-M5 09.jpg Olympus E-M5 10.jpg Olympus E-M5 11.jpg Olympus E-M5 12.jpg Olympus E-M5 13.jpg Olympus E-M5 14.jpg Olympus E-M5 15.jpg Olympus E-M5 16.jpg Olympus OM-D E-M5, Taipei, TW.jpg Olympus E-M5 + Bigma.jpg Micro Four Thirds Olympus OM-D E-M5 digital camera.jpg Oly-E-M5.jpg Olympus OM-D E-M5 Elite black kit.jpg Olympus OM-D E-M5 Elite black.jpg Olympus E-M5 with 45mm F1.8.jpg Olympus OM 50mm f1.8.jpg|E-420 Olympus-e-520-front.png|[[/Olympus E-520|Olympus E-520]] </gallery> == Modules pour smartphone == <gallery> Olympus Air A01,mounted lens and phone.jpg|Olympus Air A01 </gallery> == Objectifs à mise au point manuelle == === Série Pen === <gallery> Olympus-20mm-F3 5-with-TTL-No.jpg|[[/Olympus Zuiko 20 mm f/3,5/]] Image:PenF-Zuiko-20mm.JPG </gallery> === Série FTL (M42) === <gallery> M42.OffenblendOLYMPUS.jpg|Olympus M 42 Olympus FTL G.Zuiko 28 mm f 3,5.jpg|Olympus G.Zuiko 28 mm f/3,5 IMG.svg|Olympus Zuiko 35 mm f/2,8/ IMG.svg|Olympus Zuiko 50 mm f/1,4/ Olympus FTL F. Zuiko 50 mm f 1,8.jpg|Olympus Zuiko 50 mm f/1,8 IMG.svg|Olympus Zuiko 135 mm f/3,5/ IMG.svg|Olympus Zuiko 200 mm f/4,0/ </gallery> === Série OM-System === <gallery> OMLenses.jpg OM Zuiko f2 lenses.jpg|Zuiko f/2 </gallery> <gallery> IMG.svg|[[/Olympus Zuiko 8 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 16 mm f/3,5/]] (avant 1978) {{25}} Olympus Zuiko Auto-Macro 20mm 1-2 lens (4243439258).jpg|[[/Olympus Zuiko Auto-Macro 20 mm f/1,2/]] ZUIKO21mmF2.jpg|[[/Olympus Zuiko 21 mm f/2/]] Olympus Zuiko 2,0 21mm.jpg|[[/Olympus Zuiko 21 mm f/2/]] IMG.svg|[[/Olympus Zuiko 21 mm f/3,5/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 24 mm f/2/]] (avant 1978) {{25}} Obiettivo fotografico ultragrandangolare, messa a fuoco elicoidale, con innesto a baionetta - Museo scienza tecnologia Milano 13087.jpg|Olympus Zuiko Auto-W 24 mm f/2,8 (1991) Zuiko shift 24mm.jpg|Olympus Zuiko shift 24 mm f/3,5 à décentrement Olympus Zuiko 24mm f 2.8.JPG|[[/Olympus Zuiko 24 mm f/2,8/]] (avant 1978) {{50}} IMG.svg|[[/Olympus Zuiko 28 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 3,5 28mm.jpg|[[/Olympus Zuiko 28 mm f/3,5/]] (avant 1978) {{25}} Olympus OM 2,8 35 Shift.jpg|Olympus Zuiko shift 35 mm f/2,8 à décentrement IMG.svg|[[/Olympus Zuiko 35 mm f/2/]] (avant 1978) {{25}} Olympus G. Zuiko 2,8 35mm.jpg|[[/Olympus Zuiko 35 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 35 mm f/3,5 Macro/]] (26&nbsp;septembre&nbsp;2005) {{75}} Olympus Zuiko MC-Macro 1-3,5 f=38mm lens (4243438710).jpg|[[/Olympus Zuiko MB 38 mm f/3,5 Macro/]] Olympus OM Zuiko Zoom 3570 mm f 4,0.jpg|[[/Olympus Zuiko Auto-zoom 35-70 mm f/4/]] (1982) OM Auto Zoom 3,6 f=35-70mm-19840912-RM-123616.jpg|[[/Olympus Zuiko MC Auto-zoom 35-70 mm f/3,6/]] Olympus OM Zuiko Zoom 35-105 f 3,5-4,5.jpg|[[/Olympus Zuiko Auto-zoom 35-105 mm f/3,5/4.5 close focus/]] IMG.svg|[[/Olympus Zuiko 50 mm f/1,2/]] (1982) {{50}} Olympus Zuiko 50mm f 1.4.JPG|[[/Olympus Zuiko 50 mm f/1,4/]] (avant 1978) {{25}} Olympus OM F.Zuiko 50 mm f 1,8.jpg|[[/Olympus Zuiko 50 mm f/1,8/]] Zuiko macro50F2.jpg|[[/Olympus Zuiko Auto-Macro 50 mm f/2/]] Olympus OM 3,5 50mm Makroobjektiv.jpg|[[/Olympus Zuiko Auto-macro 50 mm f/3,5]] IMG.svg|[[/Olympus Zuiko 55 mm f/1,2/]] IMG.svg|[[/Olympus Zuiko 85 mm f/2,0/]] IMG.svg|[[/Olympus Zuiko Zoom 65-200 mm f/4/]] Olympus OM Zoom 4.0 75-150 mm.jpg|[[/Olympus Zuiko 75-150 mm f/4/]] Zuiko macro 80mm.jpg|[[/Olympus Zuiko macro 80 mm f/4/]] Olympus Zuiko 100mm f 2.8.JPG|[[/Olympus Zuiko 100 mm f/2,8/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 100 mm f/2/]] IMG.svg|[[/Olympus S Zuiko Zoom 100-200 mm f/5/]] IMG.svg|[[/Olympus Zuiko 135 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 135 mm f/3,5/]] Olympus OM Zuiko Macro 135 mm f 4,5.jpg|[[/Olympus Zuiko macro 135 mm f/4,5/]] IMG.svg|[[/Olympus Zuiko 180 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 200 mm f/5/]] Olympus Zuiko 200mm f 4.JPG|[[/Olympus Zuiko 200 mm f/4/]] (avant 1978) {{25}} IMG.svg|[[/Olympus Zuiko 250 mm f/2/]] Olympus F.Zuiko 4.5 300 mm 06.jpg|[[Olympus F Zuiko 300 mm f/4,5]] Olympus F.Zuiko 4.5 300 mm 07.jpg|[[Olympus F Zuiko 300 mm f/4,5]] IMG.svg|[[/Olympus Zuiko 350 mm f/2,8/]] IMG.svg|[[/Olympus Zuiko 400 mm f/6,3/]] IMG.svg|[[/Olympus Zuiko 600 mm f/5,6/]] IMG.svg|[[/Olympus Zuiko Reflex 500 mm f/8/]] IMG.svg|[[/Olympus Zuiko 1000 mm f/11/]] </gallery> == Objectifs autofocus == === Séries Zuiko anciennes === <gallery> IMG.svg|Olympus Zuiko AF 24 mm f/2,8 IMG.svg|Olympus Zuiko AF 28 mm f/2,8 IMG.svg|Olympus Zuiko AF 50 mm f/2,8 Macro IMG.svg|Olympus Zuiko AF 50 mm f/1,8 IMG.svg|Olympus Lens AF 28-85 mm f/3.5/4.5 IMG.svg|[[/Olympus Lens AF 35-70 mm f/3.5/4.5/]] (1982) IMG.svg|Olympus Lens AF 35-105 mm f/3.5/4.5 IMG.svg|Olympus Lens AF 70-210 mm f/3.5/4.5 (1982) </gallery> === Série Zuiko Digital === <gallery> </gallery> === Série Four-Thirds === <gallery> Olympus four thirds camera.JPG Olympus four thirds lenses.JPG IMG.svg|[[/Olympus Zuiko Digital ED 7-14 mm f/4/]] (2005) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 9-18 mm f/4-5,6/]] (2008) {{75}} Olympus lens EZ1122.jpg|[[/Olympus Zuiko Digital 11-22 mm f/2,8-3,5|Olympus Zuiko Digital 11-22 mm f/2,8-3,5]] {{75}} Olympus Zuiko Digital ED 12-60mm F2.8-4.0 SWD lens with Olympus Lens Hood LH-75B.jpg|[[/Olympus Zuiko Digital ED 12-60 mm f/2,8-4 SWD/]] (octobre 2007) {{100}} Olympus Zuiko Digital 14-42mm 3.5-5.6 ED (3795070609).jpg|[[/Olympus Zuiko Digital ED 14-42 mm f/3,5-5,6/]] (septembre 2006) {{100}} ZD 14 54 I DSC 5350.jpg|[[/Olympus Zuiko Digital 14-54 mm f/2,8-3,5/]] Olympus Zuiko Digital 14-45mm 3.5-5.6 (2179004620).jpg|[[/Olympus Zuiko Digital 14-45 mm f/3,5-5,6/]] Zuiko 14-35mm.jpg|[[/Olympus Zuiko Digital 14-35 f/2/]] Olympus Zuiko Digital 17.5-45mm 3.5-5.6 (2178211561).jpg|[[/Olympus Zuiko Digital 17,5-45 mm f/3,5-5,6/]] Olympus Zuiko Digital 25mm lens - front.jpg|[[/Olympus Zuiko Digital 25 mm f/2,8/ (Pancake)]] (mars 2008) {{100}} Olympus Zuiko Digital 35mm Macro 3.5 (2178211317).jpg|[[/Olympus Zuiko Digital 35 mm f/3,5 Macro/]] Objektiv Olympus ZUIKO DIGITAL 50mm Macro stehend.jpg|[[/Olympus Zuiko Digital ED 50 mm f/2 Macro/]] (juin&nbsp;2008) {{100}} Olympus Zuiko Digital 40-150mm f3.5-4.5 lens - front.jpg|[[/Olympus Zuiko Digital ED 40-150 mm f/3,5-4,5/]] (2006) {{100}} Olympus 50–200 2.8–3.5.jpg|50-200 mm f/2,8-3,5 ED Olympus E-500 + EC-20 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Zuiko Digital ED 50-200mm F2.8-3.5 SWD.jpg|50-200 mm f/2,8-3,5 ED Olympus E-330 + Zuiko 50-200mm.jpg|50-200 mm f/2,8-3,5 ED Objektiv Olympus ZUIKO DIGITAL 70-300mm by 300mm.jpg|[[/Olympus Zuiko Digital ED 70-300 mm f/4,0-5,6/]] (2007) {{75}} IMG.svg|[[/Olympus Zuiko Digital ED 90-250 mm f/2,8/]] (2007) {{25}} </gallery> === Série Micro Four-Thirds === <gallery> Objektiv Olympus M.ZUIKO DIGITAL 7-14mm stehend.jpg|Olympus M.Zuiko Digital 7-14 mm Objektiv Olympus M.ZUIKO DIGITAL 7-14mm.jpg MelvL P3180491 (5536855633).jpg|[[/Olympus M Zuiko Digital ED 9-18 mm f/4-5,6|Olympus M Zuiko Digital ED 9-18 mm f/4-5,6]] (avril 2010) {{50}} Olympus 9mm F8 bodycap lens on Air A01.jpg|Olympus 9 mm f/8 Olympus 9mm F8 bodycap lens on ep5.jpg Olympus 9mm F8 bodycap lens on GM5.jpg Olympus 9mm F8 Fisheye bodycap lens on E-P5.jpg IMG.svg|[[/Olympus M.Zuiko Digital ED 12 mm f/2|Olympus M.Zuiko Digital ED 12 mm f/2]] (30&nbsp;juin&nbsp;2011) {{75}} Olympus M.Zuiko Digital 14-42mm.png|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 L ED Olympus M.Zuiko Digital 14-42mm F3.5-5.6 cutted.jpg Olympus M.Zuiko digital 14-42mm f3.5-5.6 II R.jpg|[[/Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R|Olympus M.Zuiko Digital 14-42 mm f/3,5-5,6 II R]] M.Zuiko 12-50mm 02.jpg|[[/Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ|Olympus M Zuiko Digital ED 12-50 mm f/3,5-6,3 EZ]] IMG.svg|[[/Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II|Olympus M.Zuiko Digital ED 14-150 mm f/4-5,6 II]] (5&nbsp;février&nbsp;2015) {{75}} Olympus Body Cap lens 15mm F8 n01.jpg|[[/Olympus Body Cap lens 15 mm f/8|Olympus Body Cap lens 15 mm f/8]] (septembre 2012) {{100}} 2016 0212 Olympus mft 25mm1.8.jpg|[[/Olympus M-Zuiko Digital 25 mm f/1,8/]] M.Zuiko 12-50mm 01.jpg|Olympus M.Zuiko Digital 12-50 mm Olympus M.Zuiko Digital 40-150mm.png|Olympus M.Zuiko Digital 40-150 mm Olympus E-M5 15.jpg|12-50 mm Olympus M.Zuiko Digital 40-150mm F2.8.jpg|[[/Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro|Olympus M.Zuiko Digital ED 40-150 mm f/2,8 Pro]] (15&nbsp;septembre&nbsp;2014) {{100}} Olympus M Zuiko Digital ED 45mm F1.8.jpg|Olympus M Zuiko Digital ED 45 mm f/1,8 Olympus lens M.Zuiko 75 mm f1.8.jpg|[[/Olympus M.Zuiko Digital ED 75 mm f/1,8|Olympus M.Zuiko Digital ED 75 mm f/1,8]] (8&nbsp;février&nbsp;2012) {{100}} Olympus M.Zuiko Digital 300mm F4.0.jpg|Olympus M.Zuiko Digital 300 mm f/4,0 </gallery> === Objectifs spéciaux === <gallery> Image:24mmPCleft.jpg|PC 24 mm </gallery> == Compléments optiques == <gallery> File:Olympus TCON-14B.JPG|[[/Olympus TCON-14B|Olympus TCON-14B]] Complément optique télé destiné aux appareils E-10 et E-20 File:Olympus E-20 with TCON-14B.JPG|Olympus E-20 avec TCON-14B Fichier:OlympusTeleconv2x.png|Téléconvertisseur EC-20 2x File:Olympus EC-20.jpg|Téléconvertisseur EC-20 2x Image:IMG.svg|[[/Olympus EC14|Olympus EC14]] multiplicateur de focale 1,4x (2010) File:Olympus TCON-300S ohne Gegenlichtblende.JPG|Olympus TCON-300S File:Olympus E-20P mit TCON-300S.JPG|E-20P avec TCON-300S </gallery> == Flashes == <gallery> Olympus XA1 (2404583547).jpg|[[/Olympus A9M|Olympus A9M]] Olympus XA4 Macro (2388651901).jpg|[[/Olympus A11|Olympus A11 monté sur Olympus XA4 Macro]] Olympus XA3 (2405412636).jpg|[[/Olympus A16|Olympus A16 monté sur Olympus XA1]] Blixt jm2.jpg|flash T32 pour OM-2 Blixt jm3.jpg|flash T32 pour OM-2 Blixt jm4.jpg|flash T32 pour OM-2 Blixt jm5.jpg|flash T32 pour OM-2 2006-07-07 00-35-52b.jpg Olympus FL-40.jpg|FL-40 Olympus FL-40 1.jpg|FL-40 Olympus FL-40 8.jpg Olympus FL-40 7.jpg Olympus FL-40 6.jpg Olympus FL-40 5.jpg Olympus FL-40 4.jpg Olympus Blitzgerät Auto Quick 310 38.jpg|flash Auto Quick 310 pour OM-2 </gallery> == Bagues-allonges == Elles sont utilisées pour la [[proxiphotographie]] et pour la [[macrophotographie]]. * '''Olympus EX-25''' : longueur 25 mm, monture 4/3 Olympus, 150 € <gallery> File:Olympus OM Zwischenringe 25 + 14mm.jpg|Bagues de 14 et 25 mm pour Olympus OM </gallery> == Accessoires divers == <gallery> MelvL P1260102 (5388033078).jpg|Viseur électronique VF-2 MelvL P1260105 (5387430951).jpg|Viseur électronique VF-2 MelvL P3180492 (5536856995).jpg|Pare-soleil LH-55B Adattatore per esposizioni manuali - Museo scienza tecnologia Milano 13097.jpg|Adaptateur pour l'exposition manuelle pour [[/Olympus OM10/]] Olympus OM Winder 2.jpg|OM winder 2 Olympus OM MD1 Motor.jpg|OM motor drive Olympus focusing screen 1-1 (5344261324).jpg|verre de visée pour Olympus OM-1 Olympus Winkelsucher OM.jpg|Viseur d'angle pour Olympus OM Olympus-OM-Macro-Flash-Shoe-Ring.JPG|support pour flash macro Olympus slide copier hg.jpg|duplicateur de diapositives Slide copier - Olympus bellows unit, modified to take a Pentax body.jpg Slide copier - Olympus bellows unit, modified to take a Pentax body - (1).jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 06.jpg Olympus Aufbewahrungsmappe für SmartMedia Speicherkarten 08.jpg Olympus Kabelfernauslöser RM-UC1.jpg Olympus Li-ion Akkuladegerät BCM-2 21.jpg Carcasa y cámara de fotos subacuática.jpg|Caisson étanche PT-029 pour Olympus Stylus 600 (2001) </gallery> == Sacs et fourre-tout == La marque vend des étuis, sacs et fourre-tout adaptés à ses produits. {{Ph Fabricants}} 0ka3mc7xnfj6u2fo91ub4nb96bj3bx8 Neurosciences/Les synapses 0 65729 763778 760775 2026-04-16T16:55:42Z Mewtow 31375 763778 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:Gap cell junction-fr.svg|centre|vignette|upright=2.0|Jonction communicante.]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. [[File:Blausen 0843 SynapseTypes.png|centre|vignette|upright=3.0|Types de synapses.]] ===La classification des synapses en fonction de leur effet sur le neurone post-synaptique : les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles e distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> cpty929zgv22xdwe4456lhs3cyf1psg 763779 763778 2026-04-16T17:04:56Z Mewtow 31375 /* Les synapses électriques */ 763779 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. [[File:Blausen 0843 SynapseTypes.png|centre|vignette|upright=3.0|Types de synapses.]] ===La classification des synapses en fonction de leur effet sur le neurone post-synaptique : les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles e distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> ox3c93fu6sjsibv9hxvjsrd3cn80crg 763780 763779 2026-04-16T17:05:33Z Mewtow 31375 /* La classification des synapses en fonction des éléments pré- et post-synpatique */ 763780 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ===La classification des synapses en fonction de leur effet sur le neurone post-synaptique : les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles e distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> q8x3gobpj5b2qm4bhj7v3xnuiwy4xoo 763781 763780 2026-04-16T17:05:57Z Mewtow 31375 /* La classification des synapses en fonction de leur effet sur le neurone post-synaptique : les synapses excitatrices et inhibitrices */ 763781 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ===Les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles e distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> 54fh2c2o7w0qmv6sf1voteml46jir6h 763782 763781 2026-04-16T17:13:00Z Mewtow 31375 /* Les synapses excitatrices et inhibitrices */ 763782 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ===Les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> 9hdd2liiqewper5egiqadr42coz3nw4 763821 763782 2026-04-16T21:05:01Z Mewtow 31375 /* Les synapses excitatrices et inhibitrices */ 763821 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ==Les synapses électriques== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ==Les synapses chimiques== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] ===Les synapses neuronales axodendritiques classiques=== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ====La libération des neurotransmetteurs==== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ====La génération post-synaptique du potentiel d'action==== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ====La dégradation et le recyclage de neurotransmetteurs==== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La classification des synapses en fonction des éléments pré- et post-synpatique=== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ===Les synapses excitatrices et inhibitrices=== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. ==La stabilisation des synapses== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> quchlpnlfmpog61tzkvnei4tdq9ho5o 763822 763821 2026-04-16T21:06:12Z Mewtow 31375 763822 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. ==La stabilisation des synapses== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> 6eyksw161fnzs7vdm56zg46jd7q5nk9 763823 763822 2026-04-16T21:06:45Z Mewtow 31375 /* Les synapses électriques et chimiques */ 763823 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. ==La stabilisation des synapses== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> rjd7a1ztdfida545yhiqq5rblztesbp 763824 763823 2026-04-16T21:07:07Z Mewtow 31375 /* Les synapses neuronales axodendritiques classiques */ 763824 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses où l'axone se connecte au soma du neurone post-synaptique, voir sur son axone ! Et il existe même des synapses où ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. ==La stabilisation des synapses== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> bgj5xabwa0nrypolva1624a6xmw4etk 763825 763824 2026-04-16T21:11:21Z Mewtow 31375 /* Les autres formes de synapses chimiques */ 763825 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. ==La stabilisation des synapses== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> kbv3tu83ioalpwdsqi9tgcfc0xk6uj2 763826 763825 2026-04-16T21:11:57Z Mewtow 31375 763826 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La stabilisation des synapses=== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> lgopq24ahdhhdsgkakr5p5ygrs82n1z 763827 763826 2026-04-16T21:17:19Z Mewtow 31375 /* La stabilisation des synapses */ 763827 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La stabilisation des synapses=== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. Une molécule de neurexine "serre la main" à une molécule de neuroligine et les deux ne se lâchent plus. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> ggcaa373dz29d0vo94njf4hallxmtlq 763828 763827 2026-04-16T21:23:52Z Mewtow 31375 /* La stabilisation des synapses */ 763828 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La stabilisation des synapses=== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. Une molécule de neurexine "serre la main" à une molécule de neuroligine et les deux ne se lâchent plus. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] La neurexine et la neuroligine influencent la position des récepteurs synaptiques. Les récepteurs du GABA tendent à se localiser proches des molécules de neuroligine, idem pour les récepteurs du glutamate. La raison est que la neuroligine se lie à des protéines de soutien intra-cellulaire, appelées des '''protéines de densité post-synaptiques'''. Les récepteurs au GABA et au glutamate tendent à se lier eux aussi à de telles protéines. Ce qui fait que la neuroligine attire à elle des filets de protéines intracellulaires, qui "capturent" des récepteurs synaptiques. Le résultat est que la neuroligine et les récepteurs synaptiques GABA/Glutamate tendent à eux aussi se rapprocher. [[File:Handshake interaction between neuroligin and neurexin.png|centre|vignette|upright=2.5|Interaction entre neuroligine, neurexine, et proteines intra-cellulaire.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> gd7fzyj9s38w129gpd72k16avohs3s4 763829 763828 2026-04-16T21:24:24Z Mewtow 31375 /* La stabilisation des synapses */ 763829 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses neuronales axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La stabilisation des synapses=== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. Une molécule de neurexine "serre la main" à une molécule de neuroligine et les deux ne se lâchent plus. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] La neurexine et la neuroligine influencent la position des récepteurs synaptiques. Les récepteurs du GABA tendent à se localiser proches des molécules de neuroligine, idem pour les récepteurs du glutamate. La raison est que la neuroligine se lie à des protéines de soutien intra-cellulaire, appelées des '''protéines de densité post-synaptiques'''. Les récepteurs au GABA et au glutamate tendent à se lier eux aussi à de telles protéines. Ce qui fait que la neuroligine attire à elle des filets de protéines intracellulaires, qui "capturent" des récepteurs synaptiques. Le résultat est que la neuroligine et les récepteurs synaptiques GABA/Glutamate tendent à eux aussi se rapprocher. [[File:Handshake interaction between neuroligin and neurexin.png|centre|vignette|upright=2.5|Interaction entre neuroligine, neurexine, et proteines intra-cellulaire. AMPA-R et NMDA-R sont des récepteurs au glutamate.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> euhy7k40nbehmcbz256iecgzromznn4 763830 763829 2026-04-16T21:30:53Z Mewtow 31375 /* Les synapses neuronales axodendritiques classiques */ 763830 wikitext text/x-wiki Les neurones sont connectés via ce qu'on appelle des '''synapses''', qui permettent de faire passer un potentiel d'action d'un neurone vers un autre. Le neurone qui émet le potentiel d'action est appelé le neurone présynaptique, alors que le neurone qui reçoit le potentiel d'action est le neurone postsynaptique. ==Les synapses électriques et chimiques== On pourrait alors croire que les synapses sont avant tout des points de contact qui permettent aux ions de passer d'un neurone à un autre. Mais dans les faits, seule une minorité de synapses fonctionnent ainsi, la majorité des synapses passant par un intermédiaire, composé de molécules chimiques. On fait ainsi une distinction entre synapses électriques, et synapses chimiques. La différence entre les deux est illustrée ci-contre. Pour simplifier, les '''synapses électriques''' permettent au courant de passer d'un neurone à l'autre directement, alors que les '''synapses chimiques''' demandent un intermédiaire entre les deux neurones. Avec une synapse électrique, les neurones ont leur cytoplasme en continuité et peuvent échanger des ions, ce qui permet au courant de passer de l'un à l'autre sans intermédiaire. Avec une synapse chimique, les ions ne peuvent pas passer d'un neurone à l'autre. Les neurones communiquent en produisant des molécules appelées neurotransmetteurs, qui passent d'un neurone à l'autre et transmettent un signal. [[File:Electr and chem synapse.png|centre|vignette|upright=2|Synapses électriques (à gauche) et chimique (à droite).]] ===Les synapses électriques=== Les '''synapses électriques''' sont des points de contact entre deux neurones qui leur permettent d’échanger des ions. Le transfert d'un potentiel d'action d'un neurone à un autre s'effectue ainsi par conduction passive à travers le point de contact. Avec ces synapses, les canaux ioniques des deux neurones appariés : le pore d'un canal ionique est en continuité avec le pore d'un canal ionique sur l'autre neurone. Ainsi, les deux pores fusionnent et n'en forment plus qu'un. L'ensemble forme une jonction communicante. Ces jonctions peuvent s'ouvrir ou se fermer comme tout canaux ioniques. [[File:DrPaulineNeveu 02 Synapse electriq.png|centre|vignette|upright=2|synapse électrique]] Ces synapses ont l'avantage d'une transmission d'information très rapide. Mais elles ont un désavantage : les potentiels d'action peuvent passer dans les deux sens (sauf à quelques exceptions près). Elles servent le plus souvent à synchroniser des assemblées de neurones connectés entre eux. Par exemple, des assemblées de neurones qui doivent générer un rythme sont souvent reliés entre eux par des synapses électriques. C'est le cas des neurones situés sous la nuque, qui prennent en charge le rythme respiratoire, ainsi que des neurones chargés des rythmes cérébraux (les fameuses ondes cérébrales observées sur un EEG). ===Les synapses chimiques=== Les '''synapses chimiques''' déversent des substances chimiques dans leur environnement, ces molécules étant appelées des '''neurotransmetteurs'''. La grosse majorité des synapses chimiques connecte deux neurones : un neurone pré-synaptique et un neurone post-synaptique. Les neurotransmetteurs sont émis par le neurone pré-synaptique et agissent sur le neurone postsynaptique, pour créer des potentiels d'action. [[File:Basic Synapse.png|centre|vignette|upright=2|Synapse chimique classique, dite axodendritique.]] La synapse typique connecte l'axone du neurone pré-synaptique aux dendrites du neurone post-synaptique, d'où son nom de '''synapse axodendritique'''. Ce sont de loin les synapses les plus courantes, pour ne pas dire la quasi-totalité des synapses du système nerveux. Mais il existe bien d'autres types de synapses, comme on le verra à la fin du chapitre. ==Les synapses axodendritiques classiques== [[File:Synapse.png|vignette|upright=2.0|Synapse chimique de type axodendritique.]] Avec les synapses chimiques classiques, les neurones sont séparés par un espace vide : la '''fente synaptique'''. Lorsqu'un potentiel d'action arrive au bout de l'axone présynaptique, celui-ci entraîne la libération de neurotransmetteurs, qui vont se propager jusqu'au neurone postsynaptique à travers la fente. Une fois arrivés à destination, ces neurotransmetteurs vont interagir avec des molécules à la surface du neurone postsynaptique, et se lier à elles : ces molécules sont appelées des '''récepteurs synaptiques'''. Ces récepteurs synaptiques entraînent l'ouverture de canaux ioniques, ouverture qui fait varier la tension de la membrane du neurone postsynaptique : un potentiel d'action peut être déclenché sous certaines conditions. ===La libération des neurotransmetteurs=== Les neurotransmetteurs sont libérés quand un potentiel d'action atteint le bout de l'axone, le fameux bouton synaptique. Ceux-ci étaient préalablement stockés dans le neurone, dans des espèces de sac à neurotransmetteurs : les '''vésicules synaptiques'''. Il faut noter que toutes les vésicules contiennent le même nombre de molécules de neurotransmetteur. Ainsi, la quantité de neurotransmetteurs libérée dans la fente synaptique dépend uniquement du nombre de vésicules qui fusionneront avec la membrane cellulaire. Les vésicules sont stockées en deux endroits : une '''zone de réserve''' qui stocke des vésicules en surplus, et une '''zone active''' pour les vésicules destinées à être émises dans la synapse. Dans la zone de réserve, les vésicules sont liées au cytosquelette du neurone par des enzymes attachées au cytosquelette et aux vésicules. Dans la zone active, les vésicules sont accolées à la membrane du neurone et sont liées à diverses enzymes inactives. L'activation de ces enzymes, en réaction au potentiel d'action, entraîne la fusion des vésicules avec la membrane, qui déversent leur contenu à l'extérieur du neurone, dans la fente synaptique. Comme son nom l'indique, la zone de réserve stocke des vésicules de réserve au cas où la zone active se vide. Au bout de quelques dizaines de potentiels d'action, la zone active se vide de ses vésicules synaptiques. Les vésicules de la zone de réserve migrent alors pour régénérer la zone active. Mais cela prend toujours un petit peu de temps, ce qui fait qu'un neurone peut voir sa zone active entrer en pénurie. Il arrive en effet qu'un neurone soit tellement stimulé qu'il se vide de toutes ses vésicules synaptiques dans sa zone active, bien avant que la zone de réserve n'aie eu le temps d'être mobilisée. N'ayant plus de vésicules, il ne peut plus émettre de neurotransmetteurs, causant une fatigue synaptique. Nous reparlerons de ce phénomène dans le chapitre sur la plasticité synaptique. [[File:Active zone.jpg|centre|vignette|upright=2.0|Zone active et zone de réserve des vésicules synaptiques.]] Quand un potentiel d'action arrive au bout d'un axone, une cascade de réactions chimiques fait fusionner les vésicules avec la membrane de la cellule. Le mécanisme de cette fusion est relativement simple : le potentiel d'action entraîne l'ouverture de canaux ioniques calcique, le calcium introduit ainsi dans l'axone, entraînant une cascade de réactions chimiques qui fait fusionner les vésicules avec la membrane de l'axone. À ce propos, on a observé que si on privait le milieu extracellulaire de calcium, les neurones ne pouvaient pas faire fusionner leurs vésicules. Évidemment, le calcium qui est rentré dans la cellule est éliminé via des pompes calciques. Cela évite au neurone d'émettre des vésicules en continu après une première entrée de calcium. [[File:Neurotransmitter release.png|centre|vignette|upright=2.0|Libération des neurotransmetteurs dans la fente synaptique.]] ===La génération post-synaptique du potentiel d'action=== Une fois qu'ils ont traversé la fente synaptique, les neurotransmetteurs se connectent à une molécule spécialisée : un '''récepteur synaptique'''. D'ordinaire, la liaison entre un récepteur et un neurotransmetteur a tendance à faire monter la tension de membrane : cette augmentation est alors appelée un '''potentiel postsynaptique excitateur''', ou PPE. Il arrive cependant que cette liaison ait l'effet inverse : elle diminue la tension de membrane. C'est alors un '''potentiel postsynaptique inhibiteur''', ou PPI. Ces potentiels inhibiteurs tendent à empêcher un neurone d'émettre un potentiel d'action. Un neurone présynaptique peut avoir un effet qui est soit excitateur, soit inhibiteur sur le neurone postsynaptique : on parle respectivement de neurones excitateurs et inhibiteur. À tout moment, le neurone fait en quelque sorte la somme des PPE et PPI qui lui parviennent sur sa dendrite. Si celle-ci dépasse un seuil bien précis, il émet un potentiel d'action. {| |[[File:Synapse diag6.png|vignette|upright=2.0|La somme des potentiels d'entrée ne dépassent pas le seuil.]] |[[File:Synapse diag5.png|vignette|upright=1.95|La somme des potentiels d'entrée dépasse le seuil.]] |} Le dépassement du seuil a lieu si suffisamment de neurotransmetteurs sont libérés dans la fente synaptique : les effets des PPE et PPI induits par chaque neurotransmetteur s'additionnent, pouvant faire dépasser le seuil. C'est ce qu'on appelle la '''sommation spatiale''' des signaux nerveux. En plus de cette sommation spatiale, on trouve aussi une '''sommation temporelle''' : une succession très rapide de PPE ou PPI peuvent cumuler leurs effets s'ils sont très rapprochés dans le temps. [[File:Sommation Potentiel gradués.jpg|centre|vignette|upright=2.0|Sommation Potentiel gradués]] Au niveau du neurone, les récepteurs sont localisés sur une zone bien précise, située en face de l'axone présynaptique. Cette zone est appelée l''''épaississement post-synaptique''', du fait de sa forme observée au microscope. ===La dégradation et le recyclage de neurotransmetteurs=== Si les neurotransmetteurs sont libérés dans la fente synaptique, ceux-ci ne doivent pas y rester indéfiniment : si c'était le cas, une simple libération de neurotransmetteur aurait des effets durables et pourrait déclencher des PPE ou PPI durant plusieurs minutes. Il existe donc des mécanismes qui éliminent les neurotransmetteurs récemment émis de la fente synaptique. Le premier mécanisme recycle les neurotransmetteurs, les capturer pour les faire rentrer dans la cellule et les remettre en réserve dans les vésicules synaptiques. Ce système de '''recapture''' est pris en charge par les neurones présynaptiques, mais aussi par les cellules gliales. Il implique que le neurone puisse capturer des neurotransmetteurs à l'extérieur du neurone et les internaliser. Cela se fait grâce à des transporteurs, des molécules sur lesquelles le neurotransmetteur se fixe, pour être transporté dans le neurone. Un second mécanisme mécanisme consiste à dégrader les neurotransmetteurs en molécules plus simples. L'avantage est que ces molécules plus simples peuvent être recyclées par le neurone, pour reformer des neurotransmetteurs. Pour dégrader des neurotransmetteurs, il faut non seulement produire les enzymes de dégradation, mais aussi émettre ces enzymes dans la fente synaptique. Là encore, le neurone utilise des transporteurs, pour émettre les enzymes de dégradation. [[File:Generic Neurotransmitter System.jpg|centre|vignette|upright=2.0|Schéma détaillé d'une synapse.]] ===La stabilisation des synapses=== Les synapses chimiques laissent une fente synaptique entre deux neurones, ce qui fait qu'ils ne sont pas en contact direct. Si rien n'était fait, le bouton pré-synaptique pourrait bouger et se "détacher" du neurone post-synaptique. Mais cela n'arrive pas, car les synapses chimiques sont maintenues en place par des "rivets chimiques". Ces rivets chimiques sont composés de neurexine et de neuroligine, deux protéines présentes à la surface des neurones. Ce sont des '''protéines d'adhésion cellulaire''', à savoir des protéines qui collent deux cellules entre eux, ici des neurones. La neurexine est exprimée à la surface du neurone présynaptique, alors que la neuroligine est exprimée sur le neurone post-synaptique. Une molécule de neurexine "serre la main" à une molécule de neuroligine et les deux ne se lâchent plus. [[File:Cartoon of neurexin and neurolign interaction.png|centre|vignette|upright=2|Neurexin et neurolign.]] La neurexine et la neuroligine influencent la position des récepteurs synaptiques. Les récepteurs du GABA tendent à se localiser proches des molécules de neuroligine, idem pour les récepteurs du glutamate. La raison est que la neuroligine se lie à des protéines de soutien intra-cellulaire, appelées des '''protéines de densité post-synaptiques'''. Les récepteurs au GABA et au glutamate tendent à se lier eux aussi à de telles protéines. Ce qui fait que la neuroligine attire à elle des filets de protéines intracellulaires, qui "capturent" des récepteurs synaptiques. Le résultat est que la neuroligine et les récepteurs synaptiques GABA/Glutamate tendent à eux aussi se rapprocher. [[File:Handshake interaction between neuroligin and neurexin.png|centre|vignette|upright=2.5|Interaction entre neuroligine, neurexine, et proteines intra-cellulaire. AMPA-R et NMDA-R sont des récepteurs au glutamate.]] ==Les autres formes de synapses chimiques== Outre la synapse axodendritique classique, il existe d'autres types de synapses. Et il est peu dire qu'elles sont nombreuses. Vous vous imaginez sans doute des synapses entre neurones, mais les neurones peuvent faire des synapses avec d'autres cellules. Le cas le plus connu est celui d'une synapse entre un neurone et un muscle, étudié depuis longtemps et assez intuitif. Mine de rien, de telles synapses ne sont pas surprenante quand on se pose la question "comment le cerveau fait-il bouger nos muscles ?". M%ais il existe d'autres types de synapses bien moins évidentes : avec des cellules gliales, par exemple. Mais commençons par des synapses entre neurones. ===Les synapses entre neurones=== Pour faire simple, on peut classer les synapses entre neurones en deux grands types : les synapses axonales et dendritiques. Avec les synapses axonales, l'axone d'un neurone se connecte sur un autre neurone, soit sur sa dendrite, soit sur son soma, voir sur son axone ! Avec les synapses dendritiques, ce sont les dendrites du neurone pré-synaptique qui se connectent au neurone post-synaptique ! Pour les synapses axonales, un axone d'un neurone se connecte à un autre neurone, soit sur ses dendrites, sur son soma ou sur son axone. On distingue comme sous-types : * des '''synapses axodendritiques''', où un axone envoie des neurotransmetteurs à une dendrite ; * des '''synapses axoaxoniques''', qui relient deux axones ; * des '''synapses axosomatique''', qui relient un axone au corps cellulaire d'un autre neurone ; Avec les synapses dendritiques, une dendrite du neurone pré-synaptique se connecte au neurone post-synaptique, soit sur une autre dendrite, soit sur son soma. Elles sont beaucoup plus rares que les synapses axonales et on ne connaît pas bien leur utilité, aussi nous n'en parlerons pas plus que cela. On distingue comme sous-types : * des '''synapses dendrodendritiques''', qui relient deux dendrites ; * des '''synapses dendrosomatiques''', qui relient une dendrite au soma d'un autre neurone. ===Les synapses entre un neurone et un autre type de cellule=== Les synapses précédentes connectent deux neurones entre eux, ce qui fait qu'on peut les appeler des ''synapses neuronales''. Mais il existe des synapses qui connectent un neurone à autre chose, quelque chose qui n'est pas neuronal. Par exemple, les neurones moteurs sont connectés aux muscles par une synapse spéciale, appelée la ''jonction neuromusculaire''. Dans un tout autre genre, il existe des synapses hormonales, où un neurone émet des hormones/neurotransmetteurs dans le sang, histoire d'agir sur le cœur, les organes sexuels, etc. Parmi ces synapses atypiques, on trouve les suivantes : * des '''synapses avec des cellules gliales''' ; * des '''synapses axosecrétoires''', où un axone émet des substances chimiques dans le sang ; * les '''jonctions neuromusculaires''' où un neurone se connecte à un muscle pour en commander la contraction ; * des '''synapses axoextracellulaires''', où un axone émet des neurotransmetteurs dans le milieu extracellulaire. Les ''synapses axoextracellulaires''. Elles servent à l'échange d'information entre neurones et cellules gliales. Cette communication permet de réguler finement l'excitation des neurones alentours. Cela peut permettre de stabiliser un ensemble de neurones, histoire de diminuer ou d'augmenter de manière globale un ensemble de neurones. La communication est donc relativement globale, la cellule gliale ou le neurone envoyant des neurotransmetteurs à un grand nombre de neurones proches du lieu d'émission. Pour le reste, les jonctions neuromusculaires et les synapses axosecrétoires seront vues dans des chapitres ultérieurs, aussi nous n'en parlerons pas plus que cela dans ce chapitre. Ce sont surtout les synapses entre neurones qui vont nous intéresser dans la suite du cours. ==Les synapses excitatrices et inhibitrices== Pour finir, il est intéressant de faire la différence entre synapse excitatrice et inhibitrice. Les synapses excitatrices émettent des neurotransmetteurs excitateurs, c’est-à-dire qui induisent des potentiels postsynaptiques excitateurs sur le neurone post-synaptique. Elles activent le neurone post-synaptique, elles en augmentent l'activité électrique. À l'inverse, les synapses inhibitrices induisent des potentiels postsynaptiques inhibiteurs sur le neurone post-synaptique. Elles réduisent l'activité du neurone post-synaptique, elles l'inhibent. Les synapses excitatrices sont de loin les plus courantes dans le cerveau humain. On estime qu'environ 80% des synapses cérébrales sont excitatrices et 20% sont inhibitrices. Là où les choses deviennent intéressantes, c'est que les deux types de synapses n'ont pas exactement la même forme quand on les regarde au microscope. Elles se distinguent sur trois critères : la forme des vésicules synaptiques, l'épaisseur de la fente synaptique, et la forme de l'épaississement post-synaptique. * Les synapses excitatrices ont des vésicules synaptiques rondes, qui sont soit de petite taille, soit de grande taille. Elles ont une fente synaptique assez épaisse, d'une grande longueur. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est assez épaisse, de grande taille. Elles ont une forme asymétrique quand on les regarde au microscope. * Les synapses excitatrices ont à l'inverse des vésicules synaptiques aplaties. Elles ont une fente synaptique assez fine, d'une petite taille. Enfin, leur épaississement post-synaptique (la région où se trouvent les récepteurs synaptiques) est peu épais. Elles ont une forme symétrique quand on les regarde au microscope. <noinclude> {{NavChapitre | book=Neurosciences | prev=Annexe technique : les modèles mathématiques des axones | prevText=Annexe technique : les modèles mathématiques des axones | next=Les neurotransmetteurs | nextText=Les neurotransmetteurs }}{{autoCat}} </noinclude> qjxsqui6eg3nm5lrqp1ufjkmns66455 Les cartes graphiques/Les Render Output Target 0 67394 763791 763652 2026-04-16T18:11:49Z Mewtow 31375 /* Les fonctions des ROP */ 763791 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. Sa fonction la plus importante est l'élimination des pixels cachés, grâce au tampon de profondeur. Pour chaque fragment, il lit le pixel correspondant dans le tampon de profondeur, fait la comparaison de profondeur, et met à jour le tampon de profondeur. Nous en avons déjà beaucoup parlé dans les chapitres précédents, notamment dans le chapitre sur les bases du rendu 3D et dans celui sur le rastériseur (avec l'élimination précoce des pixels cachés). Une autre fonction est le mélange ''alpha'', pour gérer la transparence, qu'on a là encore vu dans le chapitre sur les bases du rendu 3D. Là encore, les ROPs lisent, pour chaque fragment, le pixel correspondant dans le ''framebuffer'', font le mélange ''alpha'', et enregistrent le résultat dans le ''framebuffer''. Pour le test ''alpha'', il était pris en charge dans les ROPs jusqu'à DirectX 9, mais est maintenant émulé dans les ''pixel shaders'' depuis DirectX 10. Mais les ROPs ont d'autres fonctions, plus méconnues, qu'on n'a pas abordé dans les chapitres précédents. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques qui existe depuis très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des ''shadowmaps'', et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur où la coordonnée z est remplacée par u octet dont le programmeur peut faire ce qu'il veut. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. Il a une certaine valeur, qui est calculée par la carte graphique, généralement par les ''shaders''. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. Les octets de ''stencil'' sont placés dans le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel correspondant, dans le tampon de profondeur. Il récupère la coordonnée z, mais aussi l'octet de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. ===Les effets de brouillard=== Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle. Notons que ce calcul implique à la fois du mélange ''alpha'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique. ===Les autres fonctions des ROPs=== Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le ''framebuffer''. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à un opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le ''framebuffer'', ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (un opérande peut être inversé). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Ils effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant le mélange ''alpha''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, mélange ''alpha''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. Un ROP est typiquement organisé comme illustré ci-dessous. Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas, mais il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] Les ROPs récupèrent les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour le mélange ''alpha'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il ne place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour le mélange ''alpha'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. Le mélange '''alpha'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} 8d0ad7v5eb40mvg5xirdr5y11odjrqi 763792 763791 2026-04-16T18:12:36Z Mewtow 31375 /* Les fonctions des ROP */ 763792 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. Sa fonction la plus importante est l'élimination des pixels cachés, grâce au tampon de profondeur. Pour chaque fragment, il lit le pixel correspondant dans le tampon de profondeur, fait la comparaison de profondeur, et met à jour le tampon de profondeur. Nous en avons déjà beaucoup parlé dans les chapitres précédents, notamment dans le chapitre sur les bases du rendu 3D et dans celui sur le rastériseur (avec l'élimination précoce des pixels cachés). Une autre fonction est le mélange ''alpha'', pour gérer la transparence, qu'on a là encore vu dans le chapitre sur les bases du rendu 3D. Là encore, les ROPs lisent, pour chaque fragment, le pixel correspondant dans le ''framebuffer'', font le mélange ''alpha'', et enregistrent le résultat dans le ''framebuffer''. Le mélange ''alpha'' est supporté sur tous les ROPs, depuis les premières cartes graphiques, et est encore supporté jusqu'à ce jour. Par contre, ce n'est pas le cas qui est du test ''alpha''. Ce dernier était pris en charge dans les ROPs jusqu'à DirectX 9, mais est maintenant émulé dans les ''pixel shaders'' depuis DirectX 10. Mais les ROPs ont d'autres fonctions, plus méconnues, qu'on n'a pas abordé dans les chapitres précédents. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques qui existe depuis très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des ''shadowmaps'', et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur où la coordonnée z est remplacée par u octet dont le programmeur peut faire ce qu'il veut. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. Il a une certaine valeur, qui est calculée par la carte graphique, généralement par les ''shaders''. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. Les octets de ''stencil'' sont placés dans le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel correspondant, dans le tampon de profondeur. Il récupère la coordonnée z, mais aussi l'octet de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. ===Les effets de brouillard=== Les '''effets de brouillard''' sont des effets graphiques assez intéressants. Ils sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. L'idée est d'avoir un ''view frustum'' limité : le plan limite au-delà duquel on ne voit pas les objets est assez proche de la caméra. Mais si le plan limite est trop proche, cela donnera une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on mélange la couleur finale du pixel avec une ''couleur de brouillard'', la couleur de brouillard étant pondérée par la profondeur. Au-delà d'une certaine distance, l'objet est intégralement dans le brouillard : le brouillard domine totalement la couleur du pixel. En dessous d'une certaine distance, le brouillard est à zéro. Entre les deux, la couleur du brouillard et de l'objet devront toutes les deux être prises en compte dans les calculs. La formule de calcul exacte varie beaucoup, elle est souvent linéaire ou exponentielle. Notons que ce calcul implique à la fois du mélange ''alpha'' mais aussi la coordonnée de profondeur, ce qui en fait que son implémentation dans les ROPs est l'idéal. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Vu que les pixels shaders peuvent s'en charger, cela fait moins de circuits dans les ROPs pour un cout en performance mineur. Et ce d'autant plus que les effets de brouillard sont devenus assez rares de nos jours. Autant les émuler dans les pixels shaders que d'utiliser des circuits pour une fonction devenue anecdotique. ===Les autres fonctions des ROPs=== Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le ''framebuffer''. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à un opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le ''framebuffer'', ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (un opérande peut être inversé). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Ils effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant le mélange ''alpha''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, mélange ''alpha''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. Un ROP est typiquement organisé comme illustré ci-dessous. Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas, mais il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] Les ROPs récupèrent les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour le mélange ''alpha'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il ne place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour le mélange ''alpha'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. Le mélange '''alpha'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} 56r7cxhrtau965281lo3fboo7toyyi8 763808 763792 2026-04-16T19:59:13Z Mewtow 31375 /* Les effets de brouillard */ 763808 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. Sa fonction la plus importante est l'élimination des pixels cachés, grâce au tampon de profondeur. Pour chaque fragment, il lit le pixel correspondant dans le tampon de profondeur, fait la comparaison de profondeur, et met à jour le tampon de profondeur. Nous en avons déjà beaucoup parlé dans les chapitres précédents, notamment dans le chapitre sur les bases du rendu 3D et dans celui sur le rastériseur (avec l'élimination précoce des pixels cachés). Une autre fonction est le mélange ''alpha'', pour gérer la transparence, qu'on a là encore vu dans le chapitre sur les bases du rendu 3D. Là encore, les ROPs lisent, pour chaque fragment, le pixel correspondant dans le ''framebuffer'', font le mélange ''alpha'', et enregistrent le résultat dans le ''framebuffer''. Le mélange ''alpha'' est supporté sur tous les ROPs, depuis les premières cartes graphiques, et est encore supporté jusqu'à ce jour. Par contre, ce n'est pas le cas qui est du test ''alpha''. Ce dernier était pris en charge dans les ROPs jusqu'à DirectX 9, mais est maintenant émulé dans les ''pixel shaders'' depuis DirectX 10. Mais les ROPs ont d'autres fonctions, plus méconnues, qu'on n'a pas abordé dans les chapitres précédents. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques qui existe depuis très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des ''shadowmaps'', et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur où la coordonnée z est remplacée par u octet dont le programmeur peut faire ce qu'il veut. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. Il a une certaine valeur, qui est calculée par la carte graphique, généralement par les ''shaders''. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. Les octets de ''stencil'' sont placés dans le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel correspondant, dans le tampon de profondeur. Il récupère la coordonnée z, mais aussi l'octet de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. ===Les autres fonctions des ROPs=== Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le ''framebuffer''. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à un opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le ''framebuffer'', ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (un opérande peut être inversé). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Ils effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant le mélange ''alpha''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, mélange ''alpha''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. Un ROP est typiquement organisé comme illustré ci-dessous. Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas, mais il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] Les ROPs récupèrent les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour le mélange ''alpha'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il ne place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour le mélange ''alpha'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. Le mélange '''alpha'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} p47olmjau519h2ez3f3ccw98lkiya96 763809 763808 2026-04-16T20:00:41Z Mewtow 31375 /* Les fonctions des ROP */ 763809 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. ===Les effets classiques du ROP=== Sa fonction la plus importante est l'élimination des pixels cachés, grâce au tampon de profondeur. Pour chaque fragment, il lit le pixel correspondant dans le tampon de profondeur, fait la comparaison de profondeur, et met à jour le tampon de profondeur. Nous en avons déjà beaucoup parlé dans les chapitres précédents, notamment dans le chapitre sur les bases du rendu 3D et dans celui sur le rastériseur (avec l'élimination précoce des pixels cachés). Une autre fonction est le mélange ''alpha'', pour gérer la transparence, qu'on a là encore vu dans le chapitre sur les bases du rendu 3D. Là encore, les ROPs lisent, pour chaque fragment, le pixel correspondant dans le ''framebuffer'', font le mélange ''alpha'', et enregistrent le résultat dans le ''framebuffer''. Le mélange ''alpha'' est supporté sur tous les ROPs, depuis les premières cartes graphiques, et est encore supporté jusqu'à ce jour. Par contre, ce n'est pas le cas qui est du test ''alpha''. Ce dernier était pris en charge dans les ROPs jusqu'à DirectX 9, mais est maintenant émulé dans les ''pixel shaders'' depuis DirectX 10. Il en est de même pour les effets de brouillard. Ils impliquent à la fois du mélange ''alpha'' mais aussi la coordonnée de profondeur, ce qui en fait que leur implémentation dans les ROPs parait logique. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Les ROPs ont d'autres fonctions, plus méconnues, qu'on n'a pas abordé dans les chapitres précédents. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques qui existe depuis très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des ''shadowmaps'', et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur où la coordonnée z est remplacée par u octet dont le programmeur peut faire ce qu'il veut. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. Il a une certaine valeur, qui est calculée par la carte graphique, généralement par les ''shaders''. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. Les octets de ''stencil'' sont placés dans le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel correspondant, dans le tampon de profondeur. Il récupère la coordonnée z, mais aussi l'octet de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. ===Les autres fonctions des ROPs=== Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le ''framebuffer''. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à un opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le ''framebuffer'', ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (un opérande peut être inversé). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Ils effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant le mélange ''alpha''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, mélange ''alpha''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. Un ROP est typiquement organisé comme illustré ci-dessous. Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas, mais il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] Les ROPs récupèrent les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour le mélange ''alpha'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il ne place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour le mélange ''alpha'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. Le mélange '''alpha'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} r5tb673nli8pimw09wx38er09s5w4iz 763815 763809 2026-04-16T20:25:52Z Mewtow 31375 /* Le z-fast pass */ 763815 wikitext text/x-wiki Pour rappel, les étapes précédentes du pipeline graphiques manipulaient non pas des pixels, mais des fragments. Pour rappel, la distinction entre fragment et pixel est pertinente quand plusieurs objets sont l'un derrière l'autre. Si vous tracez une demi-droite dont l'origine est la caméra, et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points. La couleur finale dépend de la couleur de tous ces points d'intersection. Intuitivement, l'objet le plus proche est censé cacher les autres et c'est donc lui qui décide de la couleur du pixel, mais cela demande de déterminer quel est l'objet le plus proche. De plus, certains objets sont transparents et la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Tout demande de calculer un pseudo-pixel pour chaque point d'intersection et de combiner leurs couleurs pour obtenir le résultat final. Les pseudo-pixels en question sont des '''fragments'''. Chaque fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont donc combinés pour obtenir la couleur finale de ce pixel. Pour résumer, la profondeur des fragments doit être gérée, de même que la transparence, etc. Et c'est justement le rôle de l'étage du pipeline que nous allons voir maintenant. Ces opérations sont réalisées dans un circuit qu'on nomme le '''Raster Operations Pipeline''' (ROP), aussi appelé ''Render Output Target'', situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. ==Les fonctions des ROP== Les ROP incorporent plusieurs fonctionnalités qui sont assez diverses. Leur seul lien est qu'il est préférable de les implémenter en matériel plutôt qu'en logiciel, et en dehors des unités de textures. Il s'agit de fonctionnalités assez simples, basiques, mais nécessaires au fonctionnement de tout rendu 3D. Elles ont aussi pour particularité de beaucoup accéder à la mémoire vidéo. C'est la raison pour laquelle le ROP est situé en fin de pipeline, proche de la mémoire vidéo. Voyons quelles sont ces fonctionnalités. ===Les effets classiques du ROP=== Sa fonction la plus importante est l'élimination des pixels cachés, grâce au tampon de profondeur. Pour chaque fragment, il lit le pixel correspondant dans le tampon de profondeur, fait la comparaison de profondeur, et met à jour le tampon de profondeur. Nous en avons déjà beaucoup parlé dans les chapitres précédents, notamment dans le chapitre sur les bases du rendu 3D et dans celui sur le rastériseur (avec l'élimination précoce des pixels cachés). Une autre fonction est le mélange ''alpha'', pour gérer la transparence, qu'on a là encore vu dans le chapitre sur les bases du rendu 3D. Là encore, les ROPs lisent, pour chaque fragment, le pixel correspondant dans le ''framebuffer'', font le mélange ''alpha'', et enregistrent le résultat dans le ''framebuffer''. Le mélange ''alpha'' est supporté sur tous les ROPs, depuis les premières cartes graphiques, et est encore supporté jusqu'à ce jour. Par contre, ce n'est pas le cas qui est du test ''alpha''. Ce dernier était pris en charge dans les ROPs jusqu'à DirectX 9, mais est maintenant émulé dans les ''pixel shaders'' depuis DirectX 10. Il en est de même pour les effets de brouillard. Ils impliquent à la fois du mélange ''alpha'' mais aussi la coordonnée de profondeur, ce qui en fait que leur implémentation dans les ROPs parait logique. Aussi, les premières cartes graphiques calculaient le brouillard dans les ROP, en fonction de la coordonnée de profondeur du fragment. De nos jours, il est calculé par les ''pixel shaders'' et les ROP n'incorporent plus de technique de brouillard spécialisée. Les ROPs ont d'autres fonctions, plus méconnues, qu'on n'a pas abordé dans les chapitres précédents. ===Le tampon de ''stencil''=== Le '''''stencil''''' est une fonctionnalité des API graphiques qui existe depuis très longtemps. Il sert pour générer des effets graphiques très variés, qu'il serait vain de lister ici. Il a notamment été utilisé pour calculer des ombres volumétriques (le moteur de DOOM 3 en faisait grand usage à la base), des réflexions simples, des ''shadowmaps'', et bien d'autres. Pour le résumer, on peut le voir comme une sorte de tampon de profondeur où la coordonnée z est remplacée par u octet dont le programmeur peut faire ce qu'il veut. L'idée est que chaque pixel/fragment se voit attribuer une valeur entière, généralement codée sur un octet, que les programmeurs peuvent faire varier à loisir. L'octet ajouté est appelé l''''octet de ''stencil'''''. Il a une certaine valeur, qui est calculée par la carte graphique, généralement par les ''shaders''. Il ne remplace pas la coordonnée de profondeur, mais s'ajoute à celle-ci. Les octets de ''stencil'' sont placés dans le tampon de profondeur. L'ensemble forme un tableau qui associe 32 bits à chaque" pixel : 24 bits pour une coordonnée z, 8 pour l'octet de ''stencil''. Lors du passage d'un fragment les ROPs, la carte graphique lit le pixel correspondant, dans le tampon de profondeur. Il récupère la coordonnée z, mais aussi l'octet de ''stencil''. Puis il compare l'octet de ''stencil'' avec celui du fragment traité. Si le test échoue, le fragment ne passe pas à l'étape de test de profondeur et est abandonné. S'il passe, le tampon de ''stencil'' est mis à jour. Par mis à jour, on veut dire que le ROP peut faire diverses manipulations dessus : l'incrémenter, le décrémenter, le mettre à 0, inverser ses bits, remplacer par l'octet de ''stencil'' du fragment, etc. Les opérations possibles sont bien plus nombreuses qu'avec le tampon de profondeur, qui se contente de remplacer la coordonnée z par celle du fragment. ===Les autres fonctions des ROPs=== Les ROPS implémentent aussi des techniques utilisées sur les ''blitters'' des anciennes cartes d'affichage 2D, comme l'application d''''opérations logiques''' sur chaque pixel enregistré dans le ''framebuffer''. Les opérations logiques en question peuvent prendre une à deux opérandes. Les opérandes sont soit un pixel lu dans le ''framebuffer'', soit un fragment envoyé au ROP. Les opérations logiques à un opérande peuvent inverser, mettre à 0 ou à 1 le pixel dans le ''framebuffer'', ou faire la même chose sur le fragment envoyé en opérande. Les opérations à deux opérandes lisent un pixel dans le framebuffer, et font un ET/OU/XOR avec le fragment opérande (un opérande peut être inversé). Elles sont utilisées pour faire du traitement d'image ou du rendu 2D, rarement pour du rendu 3D. Les ROPs gèrent aussi des '''masques d'écritures''', qui permettent de décider si un pixel doit être écrit ou non en mémoire. Il est possible d'inhiber certaines écritures dans le tampon de profondeur ou le tampon de couleur, éventuellement le tampon de stencil. Inhiber la mise à jour d'un pixel dans le tampon de profondeur est utile pour gérer la transparence. Si un pixel est transparent, même partiellement, il ne faut pas mettre à jour le tampon de profondeur, et cela peut être géré par ce système de masquage. Les masquages des couleurs permettent de ne modifier qu'une seule composante R/G/B au lieu de modifier les trois en même temps, pour faire certains effets visuels. ==L'architecture matérielle d'un ROP== Les ROP contiennent des circuits pour gérer la profondeur des fragments. Ils effectuent un test de profondeur, à savoir que les fragments correspondant à un même pixel sont comparés pour savoir lequel est devant l'autre. Ils contiennent aussi des circuits pour gérer la transparence des fragments. Le ROP gère aussi l'antialiasing, de concert avec l'unité de rastérisation. D'autres fonctionnalités annexes sont parfois implémentées dans les ROP. Par exemple, les vielles cartes graphiques implémentaient les effets de brouillards dans les ROPs. Le tout est suivi d'une unité qui enregistre le résultat final en mémoire, où masques et opérations logiques sont appliqués. Les différentes opérations du ROP doivent se faire dans un certain ordre. Par exemple, gérer la transparence demande que les calculs de profondeur se fassent globalement après ou pendant le mélange ''alpha''. Ou encore, les masques et opérations logiques se font à la toute fin du rendu. L'ordre des opérations est censé être le suivant : test ''alpha'', test du ''stencil'', test de profondeur, mélange ''alpha''. Du moins, la carte graphique doit donner l'impression que c'est le cas. Elle peut optimiser le tout en traitant le tampon de profondeur, de couleur et de ''stencil'' en même temps, mais donner les résultats adéquats. Un ROP est typiquement organisé comme illustré ci-dessous. Notons que les circuits de gestion de la profondeur et de la transparence sont séparés dans les schémas, mais il s'agit là d'une commodité qui ne reflète pas forcément l'implémentation matérielle. Et si ces deux circuits sont séparés, ils communiquent entre eux, notamment pour gérer la profondeur des fragments transparents. [[File:Render Output Pipeline-processor.png|centre|vignette|upright=2|Render Output Pipeline-processor]] Les ROPs récupèrent les fragments calculés par les pixels shaders et/ou les unités de texture, via un circuit d'interconnexion spécialisé. Chaque ROP est connecté à toutes les unités de ''shader'', même si la connexion n'est pas forcément directe. Toute unité de ''shader'' peut envoyer des pixels à n'importe quel ROP. Les circuits d'interconnexion sont généralement des réseaux d'interconnexion de type ''crossbar'', comme illustré ci-contre (le premier rectangle rouge). Le ROP effectue beaucoup de lectures et écritures en mémoire vidéo. Or, la bande passante mémoire est limitée, ce qui fait que le ROP est un goulot d'étranglement assez important pour le rendu 3D. Heureusement, de nombreuses optimisations permettent d'optimiser le tout. Elles agissent sur la lecture du tampon de profondeur, mais aussi sur le ''framebuffer''. ===Le ''fast clear'' du ''framebuffer''=== Une première optimisation porte sur le ''framebuffer''. Le ''framebuffer''est souvent réutilisé d'une image sur l'autre. Quand une image a été envoyée à l'écran, le ''framebuffer'' est remis à zéro pour accueillir une nouvelle image. Et ce avec ou sans ''double buffering''. La mise à zéro est censée se faire en remettant réellement le ''framebuffer'' à zéro, en écrivant des 0 pour chaque pixel du ''framebuffer''. Mais il y a moyen de s'en passer. Pour cela, l'idée est que le ''framebuffer'' est découpé en ''tiles'', des carrés de 4, 8, 16 pixels de côté. Les ''tiles'' ont généralement la même taille que les ''tiles'' utilisées pour la rastérisation, mais passons sur ce détail. L'idée est de mémoriser, pour chaque ''tile'', si elle est mise à 0 ou non. Il suffit de cela d'un seul bit par ''tile'', appelé le bit RESET. L'ensemble des bits RESET est mémorisé dans une petite mémoire SRAM, intégrée aux ROPs. Lorsqu'on souhaite remettre à zéro le ''framebuffer'', il suffit de mettre à 0 tous les bits RESET dans cette SRAM, pas besoin d’accéder à la mémoire vidéo. Avant toute lecture dans le ''framebuffer'', le ROP lit cette SRAM pour vérifier si la ''tile'' en question a été remise à 0. Si ce n'est pas le cas, il lit le pixel voulu depuis le ''framebuffer''. Mais si c'est le cas, alors le ROP ne fait pas la lecture et fournit un pixel à zéro à la place, qui est utilisé pour le mélange ''alpha'' ou autre. La moindre écriture dans une ''tile'' met le bit RESET à 0 : la ''tile'' entière est considérée comme non-remise à zéro, même si un seul pixel a été modifié dedans. Notons que l'usage d'une granularité par ''tile'' est un compromis. On peut ne peut pas utiliser un bit par pixel, car cela demanderait d'utiliser une SRAM énorme. De même, utiliser un seul bit pour tout le ''framebuffer'' ruinerait totalement l'optimisation : le ''framebuffer'' entier serait considéré comme non-RESET dès la première écriture d'un pixel dedans, on ne sauverait qu'un nombre trop limité d'accès mémoire. ===La z-compression=== La technique de '''z-compression''' compresse le tampon de profondeur. Plus précisément, elle découpe le tampon de profondeur en ''tiles'', en blocs carrés, qui sont compressés séparément les uns des autres. La taille des ''tiles'' est souvent la même que celle utilisée par le rastériseur pour la rastérisation grossière. Par exemple, la ''z-compression'' des cartes graphiques ATI radeon 9800, découpait le tampon de profondeur en ''tiles'' de 8 * 8 fragments, et les encodait avec un algorithme nommé DDPCM (''Differential differential pulse code modulation''). Précisons que cette compression ne change pas la taille occupée par le tampon de profondeur, mais seulement la quantité de données lue/écrite. La raison est que les ''tiles'' doivent avoir une place fixe en mémoire. Par exemple, si une ''tile'' non-compressée prend 64 octets, on trouvera une ''tile'' tous les 64 octets en mémoire vidéo, afin de simplifier les calculs d'adresse, afin que le ROP sache facilement où se trouve la ''tile'' à lire/écrire. Avec une vraie compression, les ''tiles'' se trouveraient à des endroits très variables d'une image à l'autre. Par contre, la z-compression réduit la quantité de données écrite dans le tampon de profondeur. Par exemple, au lieu d'écrire une ''tile'' non-compressée de 64 octets, on écrira une ''tile'' de seulement 6 octets, les 58 octets restants étant pas lus ou écrits. On obtient un gain en performance, pas en mémoire. [[File:AMD HyperZ.svg|centre|vignette|upright=2|AMD HyperZ]] Le format de compression ajoute un bit par ''tile'', qui indique si elle est compressée ou non. Le bit qui indique si la ''tile'' est compressée permet de laisser certaines ''tiles'' non-compressés, dans le cas où la compression ne permet pas de gagner de la place. La compression ajoute souvent un second bit, qui indique si la ''tile'' est à zéro ou non, sur le même modèle que pour le ''framebuffer''. Il accélère la remise à zéro du tampon de profondeur. Au lieu de réellement remettre tout le tampon de profondeur à 0, il suffit de réécrire un bit par ''tile''. Le gain en nombre d'accès mémoire peut se révéler assez impressionnant. Les deux bits en question peuvent être placés à deux endroits différents. La première solution serait d'utiliser une portion de la mémoire vidéo, mais cela demanderait de faire deux lectures par accès au tampon de profondeur. La vraie solution est d'utiliser une SRAM reliée aux ROPs, qui est assez grande pour mémoriser tout le tampon de profondeur, du moins avec deux bits par ''tile''. ===Le cache de profondeur=== Une optimisation complémentaire ajoute une ou plusieurs mémoires caches dans le ROP, dans le circuit de profondeur. Ce '''cache de profondeur''' stocke des portions du tampon de profondeur qui ont été lues ou écrite récemment. Comme cela, pas besoin de les recharger plusieurs fois : on charge un bloc une fois pour toutes, et on le conserve pour gérer les fragments qui suivent. Sur certaines cartes graphiques, les données dans le cache de profondeur sont stockées sous forme compressées dans le cache de profondeur, là encore pour augmenter la taille effective du cache. D'autres cartes graphiques ont un cache qui stocke des données décompressées dans le cache de profondeur. Tout est question de compromis entre accès rapide au cache et augmentation de la taille du cache. Il faut savoir que les autres unités de la carte graphique peuvent lire le tampon de profondeur, en théorie. Cela peut servir pour certaines techniques de rendu, comme pour le ''shadowmapping''. De ce fait, il arrive que le cache de profondeur contienne des données qui sont copiées dans d'autres caches, comme les caches des processeurs de shaders. Le cache de profondeur n'est pas gardé cohérent avec les autres caches du GPU, ce qui signifie que les écritures dans le cache de profondeur ne sont pas propagées dans les autres caches du GPU. Si on modifie des données dans ce cache, les autres caches qui ont une copie de ces données auront une version périmée de la donnée. C'est souvent un problème, sauf dans le cas du cache de profondeur, pour lequel ce n'est pas nécessaire. Cela évite d'implémenter des techniques de cohérence des caches couteuses en circuits et en performance, alors qu'elles n'auraient pas d'intérêt dans ce cas précis. ===Le ''z-fast pass''=== Le ''z-fast pass'' améliore la performance des '''prépasses z''', une technique utilisée par de nombreux moteurs de jeux vidéo. L'idée est que le moteur de jeu effectue plusieurs passes, chacune faisant un truc précis, la prépasse z étant l'une de ces passes. Lors d'une prépasse z, le moteur de jeu calcule la scène 3D, rastérise l'image, et remplit le tampon de profondeur uniquement. Il ne place pas de textures, ne calcule pas de pixels shaders, il se préoccupe uniquement des coordonnées de profondeur des pixels. Au final, le rendu ne donne que le tampon de profondeur, qui est utilisé par les passes suivantes. L'utilité est très variable, mais il y a deux raisons pour effectuer une prépasse z : la performance, mais aussi certains effets graphiques. Par exemple, les effets d'occlusion ambiante "''screen space''" utilisent le tampon de profondeur pour faire leur travail. Il en est de même pour les ''shadowmaps'', qui effectuent une prépasse z par ombre à afficher. Une autre utilisation est que cela permet d'utiliser élimination des pixels cachés très performante. On effectue une prépasse z pour calculer le tampon de profondeur final, qui est ensuite utilisé par les passes suivantes pour éliminer les pixels cachés. Ainsi, les pixels cachés ne sont pas texturés et pixel shadés, avec certitude. Toujours est-il qu'une prépasse z utilise les ROP "à moitié", dans le sens où seul le tampon de profondeur est utilisé, par la gestion des couleurs. Mais il se trouve que les circuits qui servent pour le mélange ''alpha'' peuvent être réutilisés pour faire les comparaisons de profondeur ! Le résultat est que les ROP peuvent fonctionner à double vitesse lors d'une prépasse z ! Cela demande cependant de concevoir les circuits du ROP pour en profiter. L'optimisation est parfois appelée le '''''z-fast pass'''''. Tous les GPU depuis la Geforce FX en sont capables. Il y a cependant quelques contraintes. Premièrement, le ROP doit être configuré de manière à n’accéder qu'au tampon de profondeur, ils ne doivent pas dessiner dans le ''framebuffer''. Le mélange '''alpha'' doit être désactivé, de même que l'alpha-test. D'autres contraintes supplémentaires sont parfois présentes, surtout sur les vieux GPUs. Par exemple, l'antialiasing doit être désactivé lors de la prépasse z. Et mine de rien, cela ne marche que pour les prépasses z pures. Par exemple, certaines techniques de rendu différé augmentent la prépasse z pour que celle-ci ne calcule pas que le tampon de profondeur, mais aussi d'autres informations comme les normales : elles ne profitent pas de cette optimisation. {{NavChapitre | book=Les cartes graphiques | prev=Les unités de texture | prevText=Les unités de texture | next=Les écritures en VRAM hors ROPs | nextText=Les écritures en VRAM hors ROPs }}{{autocat}} 3jqd6mms7wk6nk8lobnjl9j2ascldep Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire 0 68397 763773 762951 2026-04-16T14:12:19Z Mewtow 31375 /* L'exemple avec le x86 */ 763773 wikitext text/x-wiki Afin de gérer le partage de la mémoire sans problèmes, chaque processeur doit définir un '''modèle mémoire''', un ensemble de restrictions et de contraintes quant à l'interaction avec la mémoire RAM. La première contrainte garantit que les instructions ne puissent pas être interrompues (ou donnent l'impression de ne pas l'être) : c'est la propriété d''''atomicité'''. Un thread doit utiliser plusieurs instructions successives sur la donnée pour pouvoir en faire ce qu'il veut, et cela peut poser des problèmes si les instructions peuvent être interrompues par une exception ou tout autre chose. Par exemple, il est possible qu'une lecture démarre avant que la précédente soit terminée. De même, rien n’empêche une lecture de finir avant l'écriture précédente et renvoyer la valeur d'avant l'écriture. Prenons un exemple, avec un entier utilisé par plusieurs threads, chaque thread s’exécutant sur un processeur x86. Chaque thread veut l'incrémenter. Seul problème, l'incrémentation n'est pas effectuée en une seule instruction sur les processeurs x86. Il faut en effet lire la donnée, l'augmenter de 1, puis l'écrire. Ce qui fait que l'on peut se retrouver dans la situation illustrée ci-dessous, où un processeur n'a pas eu le temps de finir son incrémentation qu'un autre en a démarré une nouvelle. [[File:Illustration du résultat de deux opérations concurrentes sur la même variable.png|centre|vignette|upright=2|Illustration du résultat de deux opérations concurrentes sur la même variable.]] Pour avoir le bon résultat il y a une seule et unique solution : le processeur qui accède à la donnée doit avoir un accès exclusif à la donnée partagée. Sans cela, l'autre processeur ira lire une version de la donnée pas encore modifiée par le premier processeur. Dans notre exemple, un seul thread doit pouvoir manipuler notre compteur à la fois. Et bien sûr, cette réponse se généralise à presque toutes les autres situations impliquant une donnée partagée. On doit donc définir ce qu'on appelle une '''section critique''' : un morceau de temps durant lequel un thread aura un accès exclusif à une donnée partagée, avec la certitude qu'aucun autre thread ne peut modifier la donnée partagée durant ce temps. Autant prévenir tout de suite, créer de telles sections critiques se base sur des mécanismes mêlant le matériel et le logiciel. Il existe deux grandes solutions, qui peuvent être soit codées sous la forme de programmes, soit implantées directement dans le silicium de nos processeurs. L''''exclusion mutuelle''' permet à un thread de réserver la donnée partagée. Un thread qui veut manipuler une donnée réserve celle-ci, et la libère une fois qu'il en a fini avec elle. Si la donnée est réservée, tous les autres threads attendent leur tour. Pour mettre en œuvre cette réservation/dé-réservation, on ajoute un compteur à la donnée partagée, qui indique si la donnée partagée est libre ou déjà réservée. Dans le cas le plus simple, ce compteur vaudra 0 si la donnée est réservée, et 1 si elle est libre. Ce compteur ce qu'on appelle un verrou d'exclusion mutuelle, aussi appelé ''mutex'' (raccourci du terme anglais '''''mut'''ual '''ex'''clusion''). [[File:Mutex.png|centre|vignette|upright=2.5|Mutex.]] ==Les instructions atomiques== Dans le cas précédent, la vérification et modification du compteur ne peut pas être interrompue, sous peine de problèmes. On peut reprendre l'exemple du dessus pour l'illustrer. Si notre compteur est à 0, et que deux threads veulent lire et modifier ce compteur simultanément, il est possible que les deux threads lisent le compteur en même temps : ils liront alors un zéro, et essayeront alors de se réserver la donnée simultanément. Bref, retour à la case départ... Idéalement, il faudrait que lecture et écriture se fassent en une seule fois. Pour régler ce problème, certains processeurs fournissent des instructions spécialisées, in-interruptibles, capables d'effectuer cette modification du compteur en une seule fois. Elles peuvent lire le compteur, décider si on peut le modifier, et écrire la bonne valeur sans être dérangé par un autre processeur qui viendrait s'inviter dans la mémoire sans autorisation ! Par exemple, sur les processeurs x86, la vérification/modification du compteur vue plus haut peut se faire avec l'instruction ''test and set''. D'autres instructions similaires existent pour résoudre ce genre de problèmes. Leur rôle est toujours d'implémenter des verrous d'exclusion mutuelle plus ou moins sophistiqués, comme des sémaphores, des verrous (''Locks''), etc. Elles sont appelées des '''instructions atomiques'''. De telles instructions empêchent tout accès mémoire tant qu'elles ne sont pas terminées, ce qui garantit que les écritures et lectures s'exécutent l'une après l'autre. Généralement, un programmeur n'a pas à manipuler des instructions atomiques lui-même, mais manipule des abstractions basées sur ces instructions atomiques, fournies par des bibliothèques ou son langage de programmation. Voici la plupart de ces instructions atomiques les plus connues : {|class="wikitable" |- ! Instruction !! Description |- ! Compare And Swap | Cette instruction va lire une donnée en mémoire, va comparer celle-ci à l'opérande de notre instruction (une donnée fournie par l'instruction), et va écrire un résultat en mémoire si ces deux valeurs sont différentes. Ce fameux résultat est fourni par l'instruction, ou est stocké dans un registre du processeur. |- ! Fetch And Add | Cette instruction charge la valeur de notre compteur depuis la mémoire, l'incrémente, et écrit sa valeur en une seule fois. Elle permet de réaliser ce qu'on appelle des sémaphores. Elle permet aussi d'implémenter des compteurs concurrents. |- ! XCHG | Cette instruction peut échanger le contenu d'un registre et d'un morceau de mémoire de façon atomique. Elle est notoirement utilisée sur les processeurs x86 de nos PC, qui implémentent cette instruction. |} Lors de l’exécution de l'instruction atomique, aucun processeur ne peut aller manipuler la mémoire. L'instruction atomique réserve l'accès au bus en configurant un bit du bus mémoire, ou par d'autres mécanismes de synchronisation entre processeurs. Le cout de ce blocage de la mémoire est assez lourd, ce qui rend les instructions atomiques assez lentes. Mais on peut optimiser le cas où la donnée est dans le cache, en état ''Modified'' ou ''Exclusive''. Dans ce cas, pas besoin de bloquer la mémoire. Le processeur a juste à écrire dans la mémoire cache, et les mécanismes de cohérence des caches se contenteront de mettre à jour la donnée de façon atomique automatiquement. Le coût des instructions atomiques est alors fortement amorti. Sur un processeur avec désambiguïsation mémoire de type x86, il faut attendre que la file d'écriture soit vidée avant de démarrer une instruction atomique. La raison est que les instructions atomiques font une lecture, une opération, et une écriture, dans cet ordre. En théorie, la lecture peut passer avant une écriture précédente, à une adresse différente. Mais dans ce cas, cela signifierait que l'écriture aussi serait déplacée avant, car la lecture est liée à l'écriture les deux se font de manière atomique. Et faire passer une écriture avant une autre n'est pas compatible avec les règles du total store ordering. ==Les instructions LL/SC== Une autre technique de synchronisation est basée sur les instructions '''Load-Link''' et '''Store-Conditional'''. L'instruction Load-Link lit une donnée depuis la mémoire de façon atomique. L'instruction Store-Conditional écrit une donnée chargée avec Load-Link, mais uniquement à condition que la copie en mémoire n'aie pas été modifiée entre temps. Si ce n'est pas le cas, Store-conditional échoue. Pour indiquer un échec, il y a plusieurs solutions : soit elle met un bit du registre d'état à 1, soit elle écrit une valeur de retour dans un registre. Sur certains processeurs, l’exécution d'interruptions ou d'exceptions matérielles après un Load-Link fait échouer un Store-conditional ultérieur. Implémenter ces deux instructions est assez simple, et peut se faire en utilisant les techniques de ''bus-snopping'' vues dans le chapitre sur la cohérence des caches. Pour implémenter l'instruction SC, il suffit de mémoriser si la donnée lue par l'instruction LL a été invalidée depuis la dernière instruction LL. Pour cela, on utilise un registre qui mémorise l'adresse lue par l'instruction LL, à laquelle on ajoute un bit d'invalidation qui dit si cette adresse a été invalidée. L'instruction LL va initialiser le registre d'adresse avec l'adresse lue, et le bit est mis à zéro. Une écriture a lieu sur le bus, des circuits vérifient si l'adresse écrite est identique à celle contenue dans le registre d'adresse et mettent à jour le bit d'invalidation. L'instruction SC doit vérifier ce bit avant d'autoriser l'écriture. ==La mémoire Transactionnelle Matérielle== La '''mémoire transactionnelle''' permet de rendre atomiques des morceaux de programmes complets. Les morceaux de programmes rendus atomiques sont appelés des ''transactions''. Pendant qu'une transaction s'exécute, tout se passe comme si la transaction ne modifiait pas de données et restait plus ou moins "invisible" des autres processeurs. Une fois terminée, le processeur vérifie s'il y a eu un conflit d'accès avec les autres processeurs. Si c'est le cas, la transaction échoue et doit reprendre depuis le début : les changements effectués par la transaction ne seront pas pris en compte. Mais s'il n'y a pas eu conflit d'accès, alors la transaction a réussi et elle peut écrire son résultat en mémoire. Définir une transaction demande d'ajouter plusieurs instructions : une pour démarrer une transaction, une autre pour la terminer, éventuellement une troisième pour annuler précocement une transaction. Lorsqu'une transaction échoue, elle laisse la main au logiciel. Précisément, elle fait un branchement vers une fonction qui gère l'échec de la transaction. La fonction décide s'il faut ré-exécuter la transaction, attendre un peu avant de la re-démarrer, ou faire autre chose. Un autre point est que quand une transaction échoue, les registres doivent être remis à leur valeur initiale, celle d'avant la transaction. Et cette restauration est déléguée au code de gestion d'échec de transaction. Il est aussi possible de gérer la sauvegarde des registres en matériel, avec un système de ''checkpoints'', déjà abordé dans le chapitre sur les processeurs à émission dans l'ordre, dans la section sur les interruptions et le pipeline. Reste à détecter les conflits d'accès. ===La mémoire transactionnelle explicite : la réservation des lignes de cache=== La mémoire transactionnelle se décline en deux versions principales, que nous allons qualifier d'explicite et d'implicite. La mémoire transactionnelle implicite est la plus simple conceptuellement : les lectures et écritures utilisées dans la transaction sont des instructions d'accès mémoire normales, mais sont gérées de manière transactionnelle lors de la transaction. Une autre solution utilise des '''accès mémoires transactionnels explicites''', qui ajoute des instructions d'accès mémoire spécialisées pour les transactions. Il est possible d'exécuter des instructions mémoire normale dans une transaction, qui s'exécutent même si la transaction échoue. Les instructions LOAD/STORE transactionnelles sont les seules à être annulées si la transaction échoue. L'instruction de lecture transactionnelle réserve une ligne de cache pour le processeur. Par réserver, on veut dire que le processeur est le seul à avoir accès en écriture à cette ligne de cache. Si un autre processeur tente d'écrire dans cette ligne de cache, la transaction fautive est annulée. Il est aussi possible de libérer une ligne de cache réservée avec une instruction RELEASE, complémentaire des instructions de lecture/réservation. Pour résumer, il y a donc trois instructions mémoire transactionnelles : READ/RESERVE, WRITE-IF-RESERVED, et RELEASE. Vous aurez peut-être fait le lien avec les instructions LINK-LOAD et STORE-CONDITIONNAL, et il faut avouer que les instructions mémoire transactionnelles ressemblent un petit peu. Disons que ce sont des variantes adaptées au fait qu'elles s'exécutent dans des transactions. De plus, LINK-LOAD et STORE-CONDITIONNAL ne réservent pas des lignes de cache, elles se contentent de vérifier qu'une écriture n'a pas eu lieu entre la lecture et l'écriture. Pour gérer les réservations, le processeur incorpore un '''registre de réservation''' par ligne de cache réservée. Le registre mémorise la donnée lue ou écrite. Les écritures se font dans ce registre, elles ne sont pas propagées dans le cache. Par contre, il est possible que ces registres soient pris en compte par les mécanismes de cohérence des caches, afin de gérer la détection des conflits. Les transactions explicites sont plus flexibles, mais posent plus de problèmes d'implémentation que les transactions implicites. La réservation des lignes de cache est compliquée à implémenter. En comparaison, les méthodes implicites sont plus simples à comprendre et à implémenter. Nous allons nous concentrer sur les méthodes implicites dans le reste du chapitre. ===La mémoire transactionnelle implicite : les ensembles de lecture et d'écriture=== Toute transaction lit un ensemble de données appelé l'<nowiki/>'''ensemble de lecture'',''''' et écrit/modifie des données qui forment l'<nowiki/>'''ensemble d'écriture'''. Le processeur doit identifier l'ensemble de lecture et d'écriture quelque part pour détecter les conflits. Deux conditions font qu'une transaction peut échouer. La première est quand un autre processeur a écrit une donnée dans l'ensemble de lecture. La seconde est quand un autre processeur a lu ou écrit une donnée dans l'ensemble d'écriture. Par contre, il n'y a pas de problème si une donnée est lue par un autre processeur dans l'ensemble de lecture. Mémoriser l'ensemble de lecture est assez simple : il suffit d'ajouter un bit READ à chaque ligne de cache, qui indique qu'elle a été lue par une transaction. Par contre, la gestion de l'ensemble d'écriture est plus complexe. Il y a deux méthodes : une qui autorise les écritures dans le cache, une autre qui met en attente les écritures. ====L'implémentation avec un ensemble d'écritures dans la ''Store Queue''==== Une première méthode met en attente les écritures, qui ne sont propagées dans le cache qu'une fois que la transaction est terminée. Elle mémorise l'ensemble de lecture dans le cache, mais l'ensemble d'écriture reste dans la ''Store Queue'', la ''Load-Store Queue'' ou autre. La technique en question a été implémentée pour la première fois sur les processeurs Transmetta Crusoe et Efficeon, sous le nom de '''''gated store buffer'''''. Avec cette technique, les données sont bel et bien lues depuis le cache, mais les écritures ne sont pas propagées dans le cache, elles restent dans le pipeline. À la fin de la transaction, le processeur vérifie s'il n'y a pas eu de conflits et si la transaction est validée. Si ce n'est pas le cas, la ''Store Queue''/''Load-Store Queue'' est vidée, ce qui annule les changements effectués par la transaction (ROB ou autre méthode capable de gérer les interruptions précises). Si c'est le cas, les écritures sont propagées dans le cache. Si la transaction réussit, le processeur doit vider la ''Store Queue'' dedans. Les écritures se font une par une, ce qui peut prendre un peu de temps, surtout si la transaction a fait beaucoup d'écritures. Et pendant le temps, les données écrites peuvent en théorie être écrasées par une écriture provenant d'un autre cœur. Pour éviter cela, le processeur peut interdire les écritures dans tout le cache partagé pour éviter qu'un autre cœur y accède. Mais la méthode entrainerait des performances assez mauvaises. Une autre solution, systématiquement utilisée, est de seulement bloquer les lignes de cache concernées par les écritures en attente. La dernière méthode peut se mettre en œuvre en utilisant judicieusement le protocole de cohérence des caches, en exposant la ''Store Queue'' aux mécanismes de cohérence des caches. ====La mémoire transactionnelle implicite implémentée dans le cache==== Dans cette section, nous allons étudier les techniques qui utilisent le cache pour mémoriser à la fois l'ensemble de lecture et d'écriture. Avant toute chose, précisons ces techniques ont un défaut : la quantité de données manipulées par la transaction est limitée à la taille du cache. Pour éviter ce petit problème, certains chercheurs travaillent sur une mémoire transactionnelle capable de gérer des transactions de taille arbitraires. Ces techniques mémorisent les données modifiées par la transaction en mémoire RAM, dans des enregistrements que l'on appelle des logs, mais passons. Les premières propositions utilisent un cache dédié aux transactions : le '''cache transactionnel'''. À cela, il faut ajouter des instructions pour manipuler le cache transactionnel. Les données dans le cache transactionnel utilisent un protocole légèrement différent des autres caches, avec des états et des transitions en plus. Mais le cout en transistors d'un cache séparé fait que cette méthode n'a jamais été utilisée dans un processeur commercial. Une méthode moins couteuse en circuit réutilise les caches existants dans le processeur. Les écritures se font dans le cache, mais le processeur dispose d'un moyen de les annuler en cas de problème. Mais cela demande de résoudre plusieurs problèmes. Le première est la gestion des ensembles d'écritures/lecture, le second est qu'il faut annuler les écritures si une transaction échoue. Mais un point très intéressant est que la détection des conflits réutilise les mécanismes de cohérence des caches. Si un conflit est détecté pour une ligne de cache, elle est marquée comme invalide pour le protocole de cohérence des caches, et l'invalidation est propagée aux autres cœurs grâce à la liste d'adresse précédente. La détection des conflits se fait donc pendant que la transaction s'exécute, les transactions sont annulées dès qu'un conflit est détecté. Commençons par le premier problème, à savoir la détermination des ensembles de lecture/écriture. Pour cela, chaque ligne de cache possède un bit WRITE qui indique qu'elle a été écrite par une transaction. Les bits READ et WRITE sont mis à 0 au début de chaque transaction. La première écriture dans une ligne de cache met le bit WRITE à 1. Les bits READ et WRITE sont utilisés pour détecter les conflits, la détection des conflits étant prise en charge par les mécanismes de cohérence des caches. [[File:Hardware Transaction.png|centre|vignette|upright=2|Hardware Transaction]] Le second problème à résoudre est que les écritures réalisées lors d'une transaction écrasent une ligne de cache, elles en modifient le contenu. Mais si la transaction est annulée, alors il faut retrouver la ligne de cache originelle, il faut la remettre dans son état d'avant la transaction. Pour cela, la solution la plus simple est que les lignes de cache modifiées ne sont pas écrasées : leur contenu est envoyé en mémoire RAM, elles sont évincées du cache. Il faut juste configurer le cache pour qu'il sauvegarde les lignes de cache modifiées avant de faire les écritures, et ce uniquement lors des transactions. Une autre solution n'envoie pas les données en mémoire RAM, mais profite de la présence d'une hiérarchie de cache, avec la présence d'un cache partagé. Typiquement, les transactions modifient les données dans le cache L1, mais une copie de la donnée originelle est présente dans le cache L2 partagé. Sur les processeur avec un cache L3, c'est ce dernier qui est utilisé pour conserver les données non-modifiées. La raison est que ce dernier est partagé entre tous les cœurs. L'idée est que les transactions modifient les données dans les caches L1/L2 non partagés, mais propagent les écritures dans le cache partagé si la transaction réussit. Si elle échoue, les lignes de caches fautives sont marquées comme invalides par la cohérence des caches. En somme, on utilise des caches inclusifs, mais on débranche l'inclusivité du cache pendant les transactions, avant de les rétablir si la transaction réussit. Les lignes de cache marquées comme lues ou écrites par une transaction (bit READ WRITE) doivent rester dans le cache non-partagé et sont ignorées par l'algorithme de remplacement des lignes de cache. Une autre solution, utilisée sur le processeur Blue gene d'IBM, consiste à avoir plusieurs exemplaires d'une donnée dans le cache, chacun venant d'un processeur/cœur différent. Si un seul processeur a manipulé la donnée partagée, celle-ci ne sera présente qu'en une seule version dans les caches des autres processeurs. Mais si la transaction échoue, alors cela veut dire que plusieurs processeurs ont modifié cette donnée : plusieurs versions d'une même donnée différente sera présente dans le cache Voyons maintenant ce qu'il en est pour la détection des conflits. Plusieurs cas peuvent mener à un conflit et à l'abandon d'une transaction. Les cas en question surviennent quand un processeur veut écrire une donnée utilisée par une autre transaction. * Le premier cas est celui où la donnée n'a pas encore été écrite, elle est dans l'ensemble de lecture. Le premier processeur qui la modifie gagne la course, si on peut dire. Il exécute son écriture, le protocole de cohérence des caches lance une demande d'invalidation des autres copies dans les autres caches, qui fait alors automatiquement échouer les transactions sur les autres processeurs. * Le second cas est celui où la donnée a déjà été modifiée par un processeur dans son cache, mais n'est pas présente dans les autres caches non-partagés. Dans ce cas, si un second processeur veut modifier la donnée, il va aller la chercher dans le cache partagé L2/L3. Mais le protocole de cohérence des caches va remarquer que la donnée est dans le cache L1/L2 d'un autre cœur, avec le bit W mis à 1. Un conflit a alors lieu. ===Le ''speculative Lock Elision''=== Les instructions atomiques sont utilisées de façon pessimistes : l'atomicité est garantie même si aucun autre thread n'accède à la donnée lue/écrite. Aussi, pour accélérer l'exécution des instructions atomiques, des chercheurs ont trouvé une solution à ce problème de réservations inutiles, basée sur la mémoire transactionnelle. L'idée est de générer des transactions en partant des instructions atomiques présentes dans le programme. Une instruction d'acquisition d'un LOCK démarre une transaction, alors qu'une instruction atomique qui le libère termine la transaction. L'optimisation porte le nom de '''Speculative Lock Elision''', abrévié en SLE. Il s'agit d'une méthode que l'on classe à part de la mémoire transactionnelle explicite ou implicite, c'est une troisième possibilité. Pour rappel, une instruction atomique est typiquement une opération de type READ-MODIFY-WRITE : elle lit une donnée, la modifie, et écrit le résultat en mémoire. Tel est le cas de l'instruction FETCH-AND-ADD, ou de ses dérivés. Le SLE n’exécute pas l'instruction atomique permettant d'effectuer un LOCK. À la place, elle exécute une lecture normale, une opération, et l'écriture du résultat. L'écriture n'est pas propagée dans le cache, mais est mise en attente dans la ''Store Queue''. Puis, le processeur exécute les instructions qui suivent de manière transactionnelle. Quand le processeur rencontre une instruction atomique pour libérer le LOCK, il termine la transaction et vérifie si elle s'est bien passée ou doit être annulée. Les écritures en attente dans la ''store queue'' sont alors soit annulées, soit envoyées au cache et disponibles pour le mécanisme de cohérence des caches. Dans sa version non-optimisée, le SLE exécute la transaction une seule fois. Si elle échoue, l'instruction atomique est exécutée pour de vrai. Pour plus d'efficacité, certains processeurs cherchent à éviter ce genre de situation en estimant la probabilité que le premier essai (la transaction) échoue. Pour cela, ils incorporent un circuit permettant d'évaluer les chances que le premier essai marche en tant que transaction : le ''Transaction Predictor''. Une fois cette situation connue, le processeur décide ou non d’exécuter ce premier essai en tant que transaction. ===L'exemple avec le x86=== Dans cette section, nous allons étudier les premiers processeurs grand public qui ont supporté la mémoire transactionnelle matérielle : les processeurs Intel basés sur l’architecture Haswell, sortis aux alentours de mars 2013. Sur ces processeurs, deux modes sont disponibles pour la mémoire transactionnelle matérielle : le mode TSX, et le mode HLE. Le mode TSX fournit quelques instructions supplémentaires pour gérer la mémoire transactionnelle matérielle. On trouve ainsi trois nouvelles instructions : XBEGIN, XEND et XABORT. XBEGIN démarre une transaction, XEND la termine, XABOUT la fait échouer immédiatement. L'instruction XBEGIN fournit une adresse qui pointe sur un morceau de code permettant de gérer l'échec de la transaction. Les registres modifiés par une transaction échouée sont remis dans leur état initial, à une exception près : le registre EAX est utilisé pour retourner un code d'erreur qui indique les raisons de l'échec d'une transaction. Le mode HLE est celui de ''Speculative Lock Elision''. Les instructions atomiques peuvent être transformées en transaction à une condition : qu'on leur rajoute un préfixe. Le préfixe d'une instruction x86 est un octet optionnel, placé au début de l'instruction, qui permet de modifier le comportement de l'instruction. Le préfixe LOCK rend certaines instructions atomiques, REPNZE répète certaines instructions tant qu'une condition est requise, etc. Le fait est que certains préfixes n'ont pas de signification pour certaines instructions et étaient totalement ignorés par le processeur. Pour supporter le Lock Elision, ces préfixes sans significations sont réutilisés pour indiquer qu'une instruction atomique doit subir la Lock Elision. De plus, deux "nouveaux" préfixes font leur apparition : XAQUIRE qui sert à indiquer que l'instruction atomique doit être tentée en tant que transaction ; et XRELEASE qui dit que la transaction spéculative est terminée. Ainsi, un programme peut être conçu pour utiliser la Lock Elision, tout en fonctionnant sur des processeurs plus anciens, qui ne la supportent pas ! Belle tentative de garder la rétrocompatibilité. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=La cohérence des caches | prevText=La cohérence des caches | next=Un exemple de jeu d'instruction : l'extension x87 | nextText=Un exemple de jeu d'instruction : l'extension x87 }} </noinclude> 5nt1prfiuvjiw7y204wbe6utpb9inwp Fonctionnement d'un ordinateur/L'unité de contrôle 0 69026 763749 758810 2026-04-16T13:22:09Z Mewtow 31375 /* La mise à jour du microcode */ 763749 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ===Les séquenceurs câblés=== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ====L'implémentation du séquenceur==== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ====La détermination de la fin d'une instruction==== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ===Les séquenceurs microcodés=== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ====Le ''control store''==== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ====L'optimisation du microcode==== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ====Les circuits d’exécution du microcode==== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ====Localiser la première microinstruction à exécuter dans le ''control store''==== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ====La mise à jour du microcode==== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Un processeur de ce type est le GP1000 d'Imsys. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, les processeurs de ce type sont très très rares. ===Les séquenceurs hybrides=== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ====Le séquenceur hybride du 8086==== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. ==La gestion des branchements et instructions à prédicats== L'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le ''program counter'' quand un branchement est détecté. L'altération du ''program counter'' est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste. ===L'implémentation des branchements conditionnels=== Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels : * Ceux qui doivent être précédés d'une instruction de test ou de comparaison. * Ceux qui effectuent le test et le branchement en une seule instruction machine. * Ceux où les branchements conditionnels sont émulés par une ''skip instruction'', une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels.]] Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué. ====Les circuits de saut conditionnel et de calcul de la condition==== Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les '''bits intermédiaires''', et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs. [[File:Calcul d'une condition pour un branchement.png|centre|vignette|upright=1|Calcul d'une condition pour un branchement]] Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du ''program counter'' et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le ''program counter'' ou non. Le ''program counter'' doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant. [[File:Implémentation des branchements conditionnels dans le séquenceur.png|centre|vignette|upright=2|Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.]] Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple. [[File:Implémentation des branchements avec plusieurs conditions.png|centre|vignette|upright=2|Implémentation des branchements.]] Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante. ====Le registre d'état ou les registres à prédicats et les circuits associés==== Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières. * La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat. * La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement. Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change. Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul. [[File:Implémentation des branchements avec un registre d'état.png|centre|vignette|upright=2|Implémentation des branchements avec un registre d'état]] L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur. [[File:Implémentation de l'unité de chargement avec plusieurs registres à prédicats.png|centre|vignette|upright=2|Implémentation de l'unité de chargement avec plusieurs registres à prédicats]] ===L'implémentation des ''skip instructions''=== Passons maintenant au cas des ''skip instruction'', qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une ''skip instruction'' permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le ''program counter'' est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition. [[File:Implémentation des branchements pseudo-conditionnels dans le séquenceur.png|centre|vignette|upright=1.5|Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.]] ===L'implémentation des instructions à prédicats=== Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide. La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles. [[File:Implémentation des instructions à prédicats.png|centre|vignette|upright=2|Implémentation des instructions à prédicats]] La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions. [[File:Implémentation des instructions à prédicats simples.png|centre|vignette|upright=2|Implémentation des instructions à prédicats simples]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=Les architectures à accumulateur | nextText=Les architectures à accumulateur }} </noinclude> e6e4jalmdxrt9yz898maxnhmbqefpg1 763750 763749 2026-04-16T13:22:20Z Mewtow 31375 /* Les microcodes réinscriptibles */ 763750 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ===Les séquenceurs câblés=== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ====L'implémentation du séquenceur==== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ====La détermination de la fin d'une instruction==== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ===Les séquenceurs microcodés=== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ====Le ''control store''==== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ====L'optimisation du microcode==== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ====Les circuits d’exécution du microcode==== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ====Localiser la première microinstruction à exécuter dans le ''control store''==== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ====La mise à jour du microcode==== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ====Les microcodes réinscriptibles==== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Un processeur de ce type est le GP1000 d'Imsys. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, les processeurs de ce type sont très très rares. ===Les séquenceurs hybrides=== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ====Le séquenceur hybride du 8086==== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. ==La gestion des branchements et instructions à prédicats== L'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le ''program counter'' quand un branchement est détecté. L'altération du ''program counter'' est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste. ===L'implémentation des branchements conditionnels=== Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels : * Ceux qui doivent être précédés d'une instruction de test ou de comparaison. * Ceux qui effectuent le test et le branchement en une seule instruction machine. * Ceux où les branchements conditionnels sont émulés par une ''skip instruction'', une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels.]] Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué. ====Les circuits de saut conditionnel et de calcul de la condition==== Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les '''bits intermédiaires''', et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs. [[File:Calcul d'une condition pour un branchement.png|centre|vignette|upright=1|Calcul d'une condition pour un branchement]] Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du ''program counter'' et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le ''program counter'' ou non. Le ''program counter'' doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant. [[File:Implémentation des branchements conditionnels dans le séquenceur.png|centre|vignette|upright=2|Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.]] Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple. [[File:Implémentation des branchements avec plusieurs conditions.png|centre|vignette|upright=2|Implémentation des branchements.]] Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante. ====Le registre d'état ou les registres à prédicats et les circuits associés==== Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières. * La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat. * La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement. Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change. Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul. [[File:Implémentation des branchements avec un registre d'état.png|centre|vignette|upright=2|Implémentation des branchements avec un registre d'état]] L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur. [[File:Implémentation de l'unité de chargement avec plusieurs registres à prédicats.png|centre|vignette|upright=2|Implémentation de l'unité de chargement avec plusieurs registres à prédicats]] ===L'implémentation des ''skip instructions''=== Passons maintenant au cas des ''skip instruction'', qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une ''skip instruction'' permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le ''program counter'' est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition. [[File:Implémentation des branchements pseudo-conditionnels dans le séquenceur.png|centre|vignette|upright=1.5|Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.]] ===L'implémentation des instructions à prédicats=== Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide. La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles. [[File:Implémentation des instructions à prédicats.png|centre|vignette|upright=2|Implémentation des instructions à prédicats]] La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions. [[File:Implémentation des instructions à prédicats simples.png|centre|vignette|upright=2|Implémentation des instructions à prédicats simples]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=Les architectures à accumulateur | nextText=Les architectures à accumulateur }} </noinclude> 65k1a9kjlt3wu9e51onw6fvfvwpfbnh 763755 763750 2026-04-16T13:33:31Z Mewtow 31375 /* La gestion des branchements et instructions à prédicats */ Déplacement dans un autre chapitre 763755 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ===Les séquenceurs câblés=== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ====L'implémentation du séquenceur==== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ====La détermination de la fin d'une instruction==== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ===Les séquenceurs microcodés=== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ====Le ''control store''==== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ====L'optimisation du microcode==== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ====Les circuits d’exécution du microcode==== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ====Localiser la première microinstruction à exécuter dans le ''control store''==== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ====La mise à jour du microcode==== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ====Les microcodes réinscriptibles==== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Un processeur de ce type est le GP1000 d'Imsys. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, les processeurs de ce type sont très très rares. ===Les séquenceurs hybrides=== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ====Le séquenceur hybride du 8086==== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> l9w71aoaez7we8kc70lsuvb248pk4n1 763758 763755 2026-04-16T13:35:00Z Mewtow 31375 763758 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Un processeur de ce type est le GP1000 d'Imsys. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Les problèmes de compatibilité entre processeurs sont aussi légion (les programmes codés ainsi ne marchent que sur un seul processeur, pas les autres). Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Aussi, les processeurs de ce type sont très très rares. ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> 3afo8ax9mz07m88wxfg1whpv1ycpak9 763775 763758 2026-04-16T14:51:26Z Mewtow 31375 /* Les microcodes réinscriptibles */ 763775 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> svifxij5wchri8u3nf2biywgxcelm3g 763776 763775 2026-04-16T14:53:01Z Mewtow 31375 /* Les microcodes réinscriptibles */ 763776 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys, qui a été décrit entres autres dans l'article "GP1000 Has Rewritable Microcode" du magazine ''Microprocessor report''. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> cq0oc54mqt76wmbwh987dn0awdz8vl9 763777 763776 2026-04-16T15:05:52Z Mewtow 31375 /* Les microcodes réinscriptibles */ 763777 wikitext text/x-wiki Pour rappel, les instructions se font en plusieurs étapes, appelées micro-opérations. Pour chaque instruction, il faut déduire quelles sont les micro-opérations à exécuter et dans quel ordre. Mais l'instruction chargée depuis la mémoire ne précise pas les micro-opérations à faire, elle se contente juste de dire quelle opération effectuer et sur quels opérandes. Le processeur doit donc traduire l'instruction en une séquence de micro-opérations, en une séquence de signaux de commandes adéquats. C'est le rôle de l''''unité de décodage d'instruction''', une portion du processeur qui « décode » l'instruction. [[File:Unité de décodage d'instruction.png|centre|vignette|upright=2|Unité de décodage d'instruction]] Une micro-opération configure le chemin de donnée d'une manière bien précise, afin de faire une opération de base : copie entre registres, accès mémoire, opération sur l'ALU. Pour cela, il faut configurer l'ALU pour qu'elle fasse l'opération adéquate, configurer le banc de registre pour lire /écrire les bons registres, etc. La micro-opération envoie des '''signaux de commande''' adéquats au chemin de données. Pour simplifier, une micro-opération est encodée en concaténant les signaux de commande pour l'ALU, ceux pour les registres, pour l'unité mémoire, etc. Chaque micro-opération encode les signaux de commande à destination du chemin de données. {|class="wikitable" |- ! colspan="4" | Micro-opération, encodage en binaire |- | Signaux de commande pour l'ALU | Signaux de commande pour les registres | Signaux de commande pour l'unité d'accès mémoire | Autres signaux de commande |} Il existe des processeurs assez rares où chaque instruction machine est une micro-opération. Son encodage précise directement les signaux de commande, pas besoin d'une unité de décodage d'instruction. De telles architectures sont appelées des ''architectures actionnées par déplacement''. Elles feront l'objet d'un chapitre dédié, nous allons les mettre de côté pour le moment et nous concentrer sur des architectures plus courantes. ==Les séquenceurs câblés et microcodés== Pour un même jeu d'instruction, des processeurs de marque différente peuvent avoir des séquenceurs différents. Les différences entre séquenceurs sont nombreuses, une partie étant liée à des optimisations plus ou moins sophistiquées du décodage. Mais l'une d'entre elle permet de distinguer deux types purs de séquenceurs, sur un critère assez pertinent. La distinction se fait sur la nature du séquenceur, sur le circuit de décodage utilisé. Le séquenceur est un circuit séquentiel, c’est-à-dire qu'il contient un circuit combinatoire et des registres. Or, nous avons vu dans les chapitres précédents que tout circuit combinatoire peut être remplacé ainsi par une ROM avec le contenu adéquat. Et le circuit combinatoire dans le séquenceur ne fait pas exception à cette règle. Le circuit combinatoire peut être implémenté de trois grandes manières différentes. * La première méthode est d'utiliser un circuit combinatoire proprement dit, construit avec des portes logiques, en utilisant les méthodes du chapitre sur les portes logiques. * La seconde remplace ce circuit par une mémoire ROM dans laquelle on écrit la table de vérité du circuit. * La troisième solution est une solution intermédiaire qui utilise un circuit dit PLA (''Programmable Logic Array''). Il y a donc un choix à faire : est-ce le séquenceur incorpore un circuit combinatoire ou une mémoire ROM ? Cela permet de distinguer les séquenceurs câblés, basés sur un circuit combinatoire/séquentiel, et les séquenceurs microcodés, basés sur une mémoire ROM. Les deux ont évidemment des avantages et des inconvénients différents, comme nous allons le voir. ==Les séquenceurs câblés== Si les instructions sont décodées par un assemblage de portes logiques et de registres, on parle de '''séquenceur câblé'''. Plus le nombre d'instructions est important, plus un séquenceur câblé est compliqué à concevoir par rapport à ses alternatives. La complexité du séquenceur dépend aussi de la complexité des instructions machine. Autant dire que les processeurs CISC n'utilisent pas trop ce genre de séquenceurs et préfèrent utiliser des séquenceurs microcodés ou hybrides, alors que les séquenceurs câblés sont préférés sur les processeurs RISC. ===L'implémentation du séquenceur=== Sur certains processeurs assez rares, toute instruction s’exécute en une seule micro-opération, ce qui fait que le séquenceur se résume alors à un simple circuit combinatoire. C'est très rare, car cela implique que toutes les instructions doivent se faire en moins d'un cycle d'horloge. Pour cela, la durée d'un cycle d'horloge doit se caler sur l'instruction la plus lente : un accès mémoire prendra autant de temps qu'une addition, ou qu'une multiplication, etc. Ensuite, il faut que le processeur soit une architecture Harvard, afin de charge l'instruction tout en accédant aux données en parallèle, le tout en un seul cycle d'horloge processeur. [[File:Séquenceur combinatoire 01.png|centre|vignette|upright=2.5|Séquenceur combinatoire]] Sur les autres processeurs, il y a des instructions qui demandent d’exécuter une suite de micro-opérations. Pour cela, le séquenceur devient un circuit séquentiel, qui intègre un registre/compteur. La présence de ce registre s’explique par le fait que le séquenceur a besoin de savoir à quelle micro-opération il en est, information qui est mémorisée dans un registre. [[File:Séquenceur séquentiel.png|centre|vignette|upright=2|Séquenceur séquentiel]] Dans le cas le plus simple, le séquenceur est basé sur un simple compteur couplé à un circuit combinatoire. Le compteur mémorise à quelle micro-opération il en est, en lui attribuant un numéro : s'il en est à la première, seconde, troisième micro-opération, etc. Le compteur est incrémenté à chaque micro-opération réussie (les accès mémoires peuvent prendre plusieurs cycles pour une seule micro-opération, si le CPU doit attendre la RAM). Il est réinitialisé quand l'instruction se termine, à savoir quand le compteur a atteint le nombre de micro-opérations adéquat pour exécuter l'instruction. Le compteur n'est pas forcément un compteur normal, qui stocke une valeur en binaire. Il s'agit souvent d'un compteur basé un registre à décalage, appelé un '''compteur ''one-hot''''', ou encore un compteur en anneau. La raison est que les compteurs en anneau sont très rapides et utilisent peu de circuits, sans compter qu'ils permettent de se passer de comparateur pour déterminer la valeur du compteur. Leur seul défaut est que les économies en portes logiques sont contrebalancées par un plus grand nombre de bascules, qui est cependant acceptable si le compteur encode peu de valeurs. Si on veut un séquenceur qui fonctionne rapidement, en moins d'un cycle d'horloge, c'est la meilleure solution qui soit. En combinant le compteur avec l'opcode, le séquenceur détermine quel est la micro-opération à effectuer. Pour être plus précis, un circuit combinatoire intégré au séquenceur prend en entrée le compteur et l'opcode de l'instruction machine, puis fournit en sortie la micro-opération adéquate. Dans son implémentation la plus simple, ce circuit combinatoire est composé de deux sous-circuits : un décodeur et une "matrice" de portes logiques. Le décodeur prend en entrée l'opcode et a une sortie pour chaque instruction possible, ce qui fait qu'on l'appelle le '''décodeur d'instruction'''. La matrice de portes prend en entrée les sorties du décodeur et le compteur, et sort les signaux de commande adéquats. Pour chaque instruction et chaque valeur de compteur, elle sort les signaux de commande correspondant à la micro-opération adéquate. Un exemple est illustré ci-dessous. L'exemple est celui de l'exécution d'une instruction qui charge une donnée dans le registre dit accumulateur d'un processeur à accumulateur (qui n'a qu'un seul registre, le dit accumulateur). Le tout se fait en 6 cycles, dont 4 servent à gérer le chargement de l'instruction et le ''program counter''. * Le premier cycle copie le ''program counter'' dans le registre d’interfaçage pour les adresses. * Le second cycle lance une lecture, la donnée lue est sur le bus de données à la fin du cycle. * Le troisième copie l'instruction lue dans le registre d’interfaçage pour les données et dans le registre d'instruction, et incrémente le ''program counter'' en parallèle. * Le quatrième copie l'adresse à lire dans le registre d’interfaçage d'adresse. * Le cinquième lit la donnée à lire depuis la mémoire. * Le sixième copie la donnée lue du registre d’interfaçage dans l'accumulateur. [[File:Animation of an LDA instruction performed by the control matrix of a simple hardwired control unit.gif|centre|vignette|upright=2.5|Implémentation de la matrice de portes d'un séquenceur câblé. Les sorties du décodeur sont à gauche, le compteur (''one hot'') est en haut, les signaux de commandes sont émis vers le bas.]] Pour résumer, un séquenceur câblé est composé d'un compteur de micro-opération, d'un décodeur d'instruction et d'une matrice de portes logiques. Dans le schéma précédent, vous voyez que l'usage d'un compteur ''one hot'' facilite l'implémentation de la matrice de portes logiques. ===La détermination de la fin d'une instruction=== Notons que le compteur interne au séquenceur est aussi utilisé pour déterminer quand une instruction se termine. Quand une instruction se termine, le processeur doit faire deux choses : réinitialiser le compteur du séquenceur, et surtout : incrémenter le ''program counter'' pour passer à l'instruction suivante. Pour cela, on ajoute un circuit combinatoire qui détermine si l'instruction en cours est terminée. Une instruction se termine quand la dernière micro-opération est atteinte, à savoir qu'une instruction qui se termine à la énième micro-opération se termine quand le compteur atteint N. Par exemple, pour une instruction de multiplication de 6 cycles d'horloge, le décodeur sait que l'instruction est terminée le compteur atteint 5 (signe qu'il en est à sa sixième micro-opération, soit la dernière). Le circuit combinatoire qui détermine si l'instruction est terminée est donc trivial : il associe une table qui attribue pour chaque opcode le numéro de la dernière micro-opération, et un comparateur qui vérifier si le compteur a atteint cette valeur. Une manière de faire plus simple est d'utiliser un décompteur, qui est décrémenté à chaque micro-opération exécutée, et de l'initialiser avec le nombre de micro-opérations de l'instruction exécutée. L’instruction est alors terminée quand le compteur atteint zéro. Ce faisant, le circuit qui détecte la fin d'une instruction est terriblement simple, sans compter qu'il gère naturellement le cas où les instructions n'ont qu'une seule micro-opération. Mais cela n'élimine pas le circuit qui détermine le nombre de cycles d'une instruction, car celui-ci sert pour initialiser le compteur. Cette solution n'est pas toujours utilisée, pour des raisons assez diverses, notamment le fait qu'elle se marie assez mal avec diverses techniques d'optimisation. Les deux techniques précédentes fonctionnent bien à condition qu'une instruction machine corresponde toujours à la même séquence de micro-opérations. Mais ce n'est pas toujours le cas et la séquence exacte peut différer selon l'état du processeur. Le cas classique est celui des accès mémoires, où le processeur doit attendre que la donnée demandée soit lue ou écrite. Comme autre exemple, certaines étapes/micro-opérations peuvent être facultatives et ne s’exécuter que sous certaines conditions. Pensez par exemple au cas des instructions à prédicats ou des branchements. Mais on peut avoir la même chose avec des instructions de multiplication ou de division, pour lesquelles le calcul peut être plus rapide avec certains opérandes. Dans ce cas, le compteur doit pouvoir sauter certaines micro-opérations et passer par exemple de la deuxième micro-opération à la dixième directement. Et cela demande d'ajouter quelques circuits combinatoires pour cela. Par exemple, le décodeur peut incorporer une sortie pour préciser le numéro de la micro-opération suivante, ce numéro servant à réinitialiser le registre du compteur. Le séquenceur prend en entrée le compteur, l'opcode de l'instruction, éventuellement d'autres entrées, et fournit en sortie : les signaux de commande, et le prochain état du compteur. Ou alors, le décodeur d'instruction dit de combien il faut sauter de micro-opération, de combien il faut augmenter le compteur. ==Les séquenceurs microcodés== Pour limiter la complexité du séquenceur, les concepteurs de processeurs ont inventé les '''''séquenceurs microcodés'''''. L'idée derrière ces séquenceurs microcodés est que, pour chaque instruction, la suite de micro-opérations à exécuter est pré-calculée et mémorisée dans une mémoire ROM, au lieu d'être déterminée à l’exécution par un circuit combinatoire. La mémoire ROM qui stocke la suite de micro-opérations équivalente pour chaque instruction microcodée s'appelle le '''''control store''''', tandis que son contenu s'appelle le '''microcode'''. : Par abus de langage, nous parlerons parfois de microcode pour désigner la suite de microinstructions correspondant à une instruction machine. Nous parlerons alors de microcode de l'addition pour désigner la suite de microinstructions correspondant à l'instruction machine de l'addition. Faire cette petite erreur rendra la lecture de cette section beaucoup plus fluide. Les séquenceurs microcodés étaient surtout utilisés sur les architectures CISC, celles avec un jeu d'instruction étoffé et beaucoup de modes d'adressages différents. Leur grand nombre d'instructions favorisait un microcode. De plus, le budget en transistor de ces processeur était assez limité, ce qui fait que ces opérations aujourd'hui banales n'avaient pas leur propre circuit et étaient émulées en microcode. Les premiers microprocesseurs 16 bits utilisaient souvent le microcode pour implémenter des instructions comme la multiplication et la division. Un exemple est le 8086 d'Intel, qui n'avait pas de circuit multiplieur/diviseur. A la place, il émulait la multiplication avec une série d'additions et de décalages, et la division avec des soustractions/décalages. Les processeurs de ce type utilisaient un microcode pour beaucoup d'instructions, pas seulement la multiplication et la division. En conséquence, ajouter des instructions dans un microcode "existant" coutait moins cher que d'ajouter un circuit multiplieur. Un autre exemple d'utilisation du microcode est celui des premiers processeurs capables d'effectuer des calculs flottants. Sur les premiers processeurs de ce type, il n'y avait pas de FPU, pas de circuits pour les calculs flottants. Les instructions flottantes étaient en réalité émulées par des calculs entiers : chaque instruction flottante était convertie en interne en une suite d'instructions entières qui émulaient l'instruction voulue. Pour cela, les instructions flottantes étaient microcodées. De nos jours, les processeurs contiennent des circuits de calcul flottant, ce qui fait que les instructions ne sont plus émulées sauf pour quelques-unes. Les séquenceurs micro-codés sont plus simples à concevoir et simplifient beaucoup le travail des concepteurs de processeurs. L'usage du microcode permet aussi d'ajouter des instructions facilement, en modifiant le microcode, sans pour autant modifier en profondeur le processeur. En contrepartie, un séquenceur microcodé utilise plus de portes logiques, vu qu'une ROM est un circuit gourmand en portes logique. En théorie, les instructions microcodées peuvent être plus rapides que leur équivalent logiciel, à savoir une instruction émulée par une suite d'instructions machines. Le microcode peut être optimisé de manière à mieux utiliser les ressources internes au processeur. Mais force est de constater que ces opportunités d’optimisation étaient rares dans la réalité. Mais cela n'était pas l'intérêt principal, car les architectures CISC qui privilégiaient la taille du programme - la ''code size''. L'usage d'un microcode n’a plus trop d'intérêt de nos jours, et surtout pas sur les architectures RISC qui se contentent d'un séquenceur câblé. ===Le ''control store''=== La caractéristique principale du ''control store'' est sa capacité, qui est souvent assez petite. La capacité du ''control store'' dépend non seulement du nombre de micro-instructions qu'il contient, mais aussi de la taille de ces dernières. Un byte du ''control store'' correspond à une micro-instruction, les exceptions étant très très rares. Et la taille des micro-instructions varie grandement d'un processeur à l'autre. Dans les grandes lignes, la différence principale tient beaucoup la manière dont sont encodées les micro-instructions. Il existe plusieurs sous-types de séquenceurs microcodés, qui se distinguent par la façon dont sont codées les micro-opérations. * Avec le '''microcode horizontal''', chaque instruction du microcode encode directement les signaux de commande à envoyer aux unités de calcul. Vu Le grand nombre de signaux de commande, il n'est pas rare que les micro-opérations d'un microcode horizontal fassent plus d'une centaine de bits ! * Avec un '''microcode vertical''', les instructions du microcode sont traduites en signaux de commande par un séquenceur câblé qui suit le ''control store''. Son avantage est que les micro-opérations sont plus compactes, elles font moins de bits. Cela permet d'utiliser un ''control store'' plus petit ou d'avoir un microcode plus important, au détriment de la complexité du séquenceur. Un exemple de microcode vertical est le microcode du 8086, encore lui ! Pour ce qui est de la commande de l'ALU, le microcode envoie une commande abstraite qui est décodée par un circuit combinatoire (un PLA), pour obtenir les signaux de commande de l'ALU. L'implémentation interne du ''control store'' ne suit pas forcément à la lettre l'organisation en byte. Pour faire comprendre ce que je veux dire, prenons l'exemple de l'Intel 8086, dont le ''control store'' contenait 512 bytes/microinstructions de 21 bits chacune. Le ''control store'' n'était pas une ROM de 512 lignes et de 21 colonnes, comme on pourrait s'y attendre. Les dimensions 512 par 21 donneraient une ROM très allongée, rendant son placement sur la puce de silicium peu pratique. A la place, elle regroupait 4 bytes par ligne, ce qui donnait 84 lignes et 128 colonnes. ===L'optimisation du microcode=== Le ''control store'' a souvent une capacité très faible, même pour une mémoire ROM. Une ROM prend de la place, ce qui fait que les concepteurs de processeurs préfèrent utiliser une ROM assez petite. Néanmoins, malgré la petitesse des ROM de l'époque, il arrivait souvent que le ''control store'' contienne des vides, des bytes inoccupés. Cela arrive si le microcode n'a pas une taille égale à une puissance de deux. Par exemple, si l'on a un microcode qui occupe 120 bytes, on doit utiliser un ''control store'' de 128 bytes, ce qui laisse 8 bytes vides. On pourrait croire que les vides sont placés à la fin du ''control store'', mais il est parfois préférable de disperser les vides dans le ''control store'', afin de simplifier les circuits adossés au microcode, ce que nous allons voir dans ce qui suit. Pour les concepteurs de processeurs, une difficulté majeure est de faire rentrer le microcode dans le ''control store''. C'est encore un problème à l'heure actuelle, mais ce l'était encore plus sur les architectures anciennes, qui devaient faire avec des ROM limitées qu'actuellement. De plus, sur les anciennes architectures CISC, le grand nombre d'instructions recherchait se mariait mal à la petite capacité des mémoires ROM de l'époque. Les concepteurs de processeurs devaient ruser pour faire rentrer un microcode souvent complexe dans une petite ROM. Diverses optimisations étaient possibles. La première optimisation de ce genre consiste à gérer des fonctions/sous-programmes/routines logicielles dans le microcode. Pour cela, les circuits en charge du microcode géraient l’exécution de fonctions dans le microcode, avec des registres pour l'appel de retour, des microinstructions pour faire des branchements dans le microcode et tout ce qui va avec. Mais le tout était généralement simplifié et rares étaient les processeurs qui incorporaient une pile d'appel complète pour le microcode. Beaucoup se limitaient à ajouter un registre pour l'adresse de retour, quelques instructions de branchement interne au microcode, et guère plus. Un exemple assez intéressant est celui du processeur Intel 8086, dont le microcode contient une sous-routine pour gérer chaque mode d'adressage. Sans optimisations, il faudrait un microcode par instruction et par mode d'adressage. Par exemple, le microcode pour une addition en mode d'adressage immédiat n'est pas la même que pour une instruction d'addition en mode d'adressage direct. Cependant, elles partagent un même cœur qui s'occupe de l'addition et de la gestion de l'accumulateur, même si la gestion des opérandes est totalement différente suivant le mode d'adressage. Pour éliminer cette redondance, le microcode du 8086 délègue la gestion des modes d'adressages et des opérandes à des sous-programmes spécialisés, une par mode d'adressage. La seconde optimisation est de réduire la taille des micro-instructions en jouant sur leur encodage. L'usage d'un microcode vertical est une première solution. Décoder certaines instructions simples sans passer par le microcode en est une autre, et elle donne les séquenceurs hybrides dont nous parlerons dans la suite du chapitre. Mais d'autres techniques sont possibles, comme le fait de déporter une partie du décodage en-dehors du ''control store'', dans des circuits logiques séparés. Un bon exemple de cela est celui de l'Intel 8086, encore lui, sur lequel beaucoup d'instructions existaient en deux exemplaires : une version 8 bits et une version 16 bits. Il n'y avait pas de microcode séparé pour les deux versions, mais un seul microcode qui s'occupait autant de la version 8 bits que de la version 16 bits de l'instruction. La différence entre les deux se faisait au niveau du bus interne du processeur. Un bit de l'instruction machine indiquait s'il s'agissait d'une version 8 ou 16 bits et ce bit était transmis à la machinerie du bus interne, sans passer par le microcode. ===Les circuits d’exécution du microcode=== Le processeur doit trouver un moyen de dérouler les micro-instructions les unes après les autres, ce qui est la même chose qu'avec des instructions machines. Le micro-code est donc couplé à un circuit qui de l’exécution des micro-opérations les unes après les autres, dans l'ordre. Ce circuit est l'équivalent du circuit de chargement, mais pour les micro-opérations. Pour cela, il y a deux méthodes, que voici. La première méthode fait que chaque micro-instruction contient l'adresse de la micro-instruction suivante. Avec cette méthode, on peut disperser une suite de microinstructions dans le ''control store'', au lieu de garder des microinstructions consécutives. L'utilité de cette méthode n'est pas évidente, mais elle deviendra plus claire dans la section suivante. [[File:Microcode sans microséquenceur.gif|centre|vignette|upright=1.5|Microcode sans microséquenceur.]] La seconde méthode fait que le séquenceur contient un équivalent du ''program counter'' pour le microcode. On trouve ainsi un '''micro-séquenceur''' qui regroupe un '''registre d’adresse de micro-opération''' et un '''micro-compteur ordinal'''. Le registre d’adresse de micro-opération est initialisé avec l'opcode de l'instruction à exécuter, qui pointe vers la première micro-instruction. Le micro-compteur ordinal se charge d'incrémenter ce registre à chaque fois qu'une micro-instruction est exécutée, afin de pointer sur la suivante. [[File:Microcode avec un microséquenceur.gif|centre|vignette|upright=2|Microcode avec un microséquenceur.]] Un séquenceur microcodé peut même gérer des micro-instructions de branchement, qui précisent la prochaine micro-instruction à exécuter. Grâce à cela, on peut faire des boucles de micro-opérations, par exemple. Pour gérer les micro-branchements, il faut rajouter la destination d'un éventuel branchement dans les micro-instructions de branchement. La taille des micro-instructions augmente alors, vu que toutes les micro-opérations ont la même taille. Voici ce que cela donne pour les microcodes avec un microcompteur ordinal. On voit que l'ajout des branchements modifie le microcompteur ordinal de façon à permettre les branchements entre micro-opérations, d'une manière identique à celle vue pour l'unité de chargement. [[File:Branchements avec microcode horizontal avec microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal avec microséquenceur.]] Voici ce que cela donne pour les microcodes où chaque micro-instruction contient l'adresse de la suivante : [[File:Branchements avec microcode horizontal sans microséquenceur.gif|centre|vignette|upright=2|Branchements avec microcode horizontal sans microséquenceur.]] Il est possible de créer des fonctions/sous-programmes/sous-routines dans le microcode, grâce à ces micro-branchements et en ajoutant un registre pour gérer l'adresse de retour. ===Localiser la première microinstruction à exécuter dans le ''control store''=== Un premier problème à résoudre avec un microcode, est de localiser la suite de micro-instructions à exécuter. Si l'on veut exécuter une instruction machine, le microcode doit trouver le début de la suite de microinstruction dans le microcode et démarrer l’exécution des microinstructions à partir de là. Pour le dire autrement, le séquenceur doit déterminer, à partir de l'opcode, quelle est l'adresse de départ dans le ''control store''. Pour cela, il y a plusieurs solutions. La première solution fait une traduction de l'opcode vers l'adresse de départ, en utilisant un circuit combinatoire et/ou une mémoire ROM. Elle a l'inconvénient de complexifier le processeur, dans le sens où on doit ajouter des circuits en plus. De plus, le circuit ou la ROM ajoutés mettent un certain temps avant de donner leur résultat, ce qui ralentit quelque peu le décodage des instructions. L'avantage principal est que l'on peut utiliser facilement un microséquenceur basique et placer les microinstructions les unes à la suite des autres dans le ''control store''. Cette technique s'utilise aussi bien avec un micro-séquenceur que sans. Dans les faits, elle s'utilise de préférence avec un micro-compteur ordinal. L'usage de ce dernier réduit fortement la taille du ''control store'', ce qui compense le fait de devoir ajouter des circuits pour faire la traduction opcode -> adresse. [[File:Control store adressé par predecodage de l'opcode.png|centre|vignette|upright=2|Control store adressé par predecodage de l'opcode]] L'autre solution considère l'opcode de l'instruction microcodée comme une adresse : le ''control store'' est conçu pour que cette adresse pointe directement sur le début de la suite de micro-opérations correspondante, la première micro-instruction de cette suite. Du moins, c'est le principe général, mais un détail vient mettre son grain de sel : un ''control store'' utilise systématiquement des adresses plus grandes que l'opcode. Ce qui fait qu'il faut rajouter des bits à l'opcode pour obtenir l'adresse, on doit concaténer des zéros à l'opcode pour obtenir l'adresse finale. On fait alors face à deux choix : soit on met l'opcode dans les bits de poids faible de l'adresse, soit on la place dans les bits de poids fort. Les deux solutions ont des avantages et inconvénients différents. [[File:Control store.gif|centre|vignette|upright=2|Control store d'un microcode horizontal.]] La première méthode place les opcodes dans les bits de poids faible et les zéros dans les bits de poids fort. Le défaut principal de cette méthode vient du fait que de nombreux opcodes ont des représentations binaires proches, ce qui fait que leurs adresses de départs sont proches dans le ''control store''. Il n'y a alors pas assez d'espace entre les deux adresses de départ pour y placer une suite de microninstructions. En clair, cette méthode ne peut pas s'utiliser avec un micro-séquenceur. Par contre, elle se marie très bien avec un ''control store'' où chaque microinstruction contient l'adresse de la suivante. En faisant cela, l'opcode pointe vers l'adresse de départ, mais le reste de la suite de microinstructions est placé ailleurs dans le ''control store'', dans des adresses qui ne correspondent pas à des opcodes. Les adresses de départ occupent donc le bas de la ROM du ''control store'', alors que le haut de la ROM contient les suites de microinstructions et éventuellement des vides. [[File:Control store adressé par l'opcode - opcode sur bits de poids faible.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids faible]] La seconde méthode met l'opcode dans les bits de poids fort de l'adresse et les zéros dans les bits de poids faible. En faisant cela, les adresses de départ sont dispersées dans le ''control store'', elles sont séparées par des intervalles de taille de fixe. Cela garantit qu'il y a un espace fixe entre deux adresses de départ, dans lequel on peut placer une suite de microinstructions. Un bon exemple est celui du 8086, dont le microcode, très complexe, espace chaque instruction/opcode tous les 16 bytes, ce qui permet d'avoir 16 microinstructions par instruction machine. Son ''control store'' contenait 512 micro-instructions, 512 bytes, ce qui donne des adresses de 13 bits. Mais l'opcode occupait les 9 bits de poids fort de l'adresse de microcode, ce qui laissait 4 bits de poids faible libres. En conséquence, chaque instruction machine disposait de maximum 16 microinstructions consécutives. L'avantage de cette méthode est que l'on peut utiliser un microséquenceur plus petit, avec un incrémenteur de plus petite taille. De plus, les adresses utilisées pour les branchements dans le microcode sont plus petites. Par exemple, le microcode du 8086, qui espacait ses microinstructions toutes les 16 bytes, avait un microséquenceur de 4 bits. Ce dernier contenait un incrémenteur de micro-''program counter'' de 4 bits et non 13. De plus, les adresses utilisées pour les branchements dans le microcode ne faisaient que 4 bits, à savoir qu'il s'agissait de branchements relatifs. Tout cela rendait le microséquenceur beaucoup plus économe en circuits. Cette solution a cependant pour défaut de laisser beaucoup de vides dans le ''control store''. Le microcode de certaines instructions était assez court, d'autres avaient un microcode plus long. L'espace entre deux opcodes, entre deux adresses de départ, est fixe et se cale sur le microcode le plus long. En conséquence, le microcode de certaines instructions laisse des vides à sa suite. Si on sépare les adresses de départ par un espace assez court, alors les suites d'instructions trop longues ne rentrent pas, sauf en trichant. Par tricher, on veut dire que le microcode de ces instruction est découpé en morceaux et dispersé dans les vides du ''control store''. L’exécution d'un microcode dispersé ainsi se fait normalement grâce aux microinstructions de branchement. [[File:Control store adressé par l'opcode - opcode sur bits de poids fort 01.png|centre|vignette|upright=2|Control store adressé par l'opcode - opcode sur bits de poids fort]] Pour comparer les trois méthodes, on peut comparer ce qu'il en est pour le remplissage du ''control store''. Les deux premières méthodes remplissent le ''control store'' au mieux, alors que la dernière laisse des vides et disperse les suites de microinstructions dans le ''control store''. Par contre, il faut aussi tenir compte d'autres paramètres. La première solution demande d'ajouter des circuits de traduction opcode -> adresse qui prennent de la place, pas les deux dernières solutions. Enfin, la deuxième solution impose de rallonger les bytes du ''control store'', car on se prive de micro-séquenceur, ce qui n'est pas le cas des deux autres. Au final, comparer les trois solutions ne donne pas de gagnant absolu : tout dépend de l'implémentation du jeu d'instruction choisit, de son encodage, etc. ===La mise à jour du microcode=== Parfois, le processeur permet une mise à jour du ''control store'', ce qui permet de modifier le microcode pour corriger des bugs ou ajouter des instructions. L'utilisation principale est de corriger des bugs ou des problèmes de sécurité assez tordus. Il est fréquent que les processeurs aient des bugs matériels, présents à cause de défauts de conception parfois subtils. Les grands fabricants comme Intel et AMD documentent ces bugs dans leur documentation officielle. Une petite partie de ces bugs peuvent se corriger avec une mise à jour du microcode, et ils ne sont pas forcément dans le microcode lui-même. Un exemple serait la désactivation des instructions TSX sur les processeurs x86 Haswell, en 2014, qui ont été désactivées par une mise à jour du microcode, après qu'un bug de sécurité ait été découvert. La mise à jour du microcode est rarement permanente. Une mise à jour permanente du microcode implique que le ''control store'' est une EEPROM ou une mémoire ROM reprogrammable, donc des mémoire très difficiles à mettre en œuvre dans les processeurs. Or, le ''control store'' doit être une mémoire extrêmement performante, capable de fonctionner à très haute fréquence, avec des temps d'accès minuscules, aux performances proches d'une SRAM. En réalité, le ''control store'' est mis à jour temporairement, et est réinitialisé à chaque boot de l'ordinateur, à chaque boot du processeur. Pour cela, le ''control store'' est implémenté avec deux mémoires : une ROM qui contient le microcode originel, et une SRAM. Pour simplifier les explications, nous allons appeler ces deux mémoires la micro-ROM et la micro-RAM. Au démarrage de l'ordinateur, le microcode contenu dans la micro-ROM est copié dans la micro-RAM. Peu après l'allumage du processeur, le contenu de la micro-RAM peut être remplacé par un microcode mis à jours. Typiquement, le microcode corrigé est fourni soit par le BIOS, soit par le système d'exploitation. Les mises à jour de microcode sont généralement soumises à des mesures de sécurité drastiques intégrées au processeur. Par exemple, le microcode fournit par le fabricant est chiffré avec des clés connues seulement des fabricants de CPU, autres), et un microcode n'est chargé par le processeur que si la clé correspond. ===Les microcodes réinscriptibles=== Il existe des processeurs dont le microcode est directement reprogrammable, accessible par le programmeur. Le programmeur peut écrire ce qu'il veut dans la micro-RAM, à sa guise. On peut ainsi changer le jeu d'instruction du processeur au besoin, afin d'ajouter des instructions utiles. Il s'agit de processeurs destinés à l'embarqué, qui doivent être conçus sur mesure, on ne trouve pas de systèmes de ce genre dans les PCs. Ils sont appelés des '''processeurs à microcode réinscriptible'''. L'utilité est que les programmes peuvent disposer des instructions les plus adéquates pour leur fonction, ce qui réduit la taille du code (la mémoire prise par le programme exécutable) et facilite la programmation en assembleur. Ces deux avantages n'ont pas grand intérêt de nos jours. De plus, l'utilisation de cette technique demande un ''control store'' assez imposant, de grande taille, rarement rapide. Par contre, cette fonctionnalité a de nombreux défauts. Si chaque programme peut changer à la volée le jeu d'instruction du processeur, cela peut mettre le bazar. Si un programme change le microcode, les programmes qui passent après lui doivent réinitialiser le microcode pour ne pas exécuter des instructions incorrectes. Cela peut aussi poser des problèmes de sécurité, les hackers étant doués pour utiliser ce genre de fonctionnalités à des fins malveillantes. Cependant, les processeurs de ce type sont utilisés pour l'embarqué, sur des systèmes où un seul programme s'exécute, sans accès à un réseau local, il n'y a donc pas de problèmes liés à des programmes malveillants ou des interactions entre programmes. Un processeur de ce type est le GP1000 d'Imsys, qui a été décrit entres autres dans l'article "GP1000 Has Rewritable Microcode" du magazine ''Microprocessor report''. Il disposait d'une micro-ROM de 36 kibioctets, couplées à une micro-RAM de 18 kibioctets. Une partie du microcode était donc réinscriptible : un tiers l'était, les deux autres tiers étaient dans une micro-ROM impossible à modifier. Il y avait donc un microcode permanent en micro-ROM et un microcode variable en en micro-RAM. Le microcode permanent contient un ''chargeur de microcode'', qui recopie le microcode variable dans la micro-RAM, à l'allumage de l'ordinateur. Par la suite, le microcode variable est chargé avec une instruction machine dédiée. Un point très original est que le GP1000 supporte l'exécution de plusieurs programmes à tour de rôle. Le microcode permanent contient de quoi gérer des interruptions, émuler des périphériques virtuels, mais aussi gérer la présence de plusieurs programmes en cours d'exécution. Les programmes s'exécutent à tour de rôle et le microcode décide qui s’exécute et pour combien de temps. Le microcode permanent contient donc une sorte de mini-système d'exploitation rudimentaire ! Au-delà ce ça, Imsys fournissait des microcodes près à l'emploi, qui pouvaient être chargés à la demande. L'un d'entre eux permettait d'implémenter la machine virtuelle Java directement dans le processeur. Pour information, la machine virtuelle Java standardise le langage machine d'un processeur fictif, précisément une machine à pile simple, qui peut en théorie être implémentée en matériel. C'est exactement ce que faisait l'un des microcodes près à l'emploi d'Imsys. D'autres exemples de processeurs à microcode réinscriptible sont les Burroughs B1700, ainsi que le LSI-11 de l'entreprise Digital. Pour ce dernier, son ''control store'' est décrit dans l'article "TheLSI-11/23 Control Store Microarchitecture", qui n'est malheureusement pas accesible gratuitement. ==Les séquenceurs hybrides== Les séquenceurs hybrides sont un compromis entre séquenceurs câblés et microcodés. Ils permettent de profiter des avantages et inconvénients des deux types de séquenceurs. Sur le principe, une partie des instructions est décodée par une partie câblée, et l'autre passe par le microcode. Typiquement, de tels séquenceurs sont très fréquents sur les architectures CISC, où ils permettent un décodage rapide pour les instructions simples, alors que les instructions complexes le sont par le microcode, plus lent. L'organisation interne d'un séquenceur hybride varie grandement selon le processeur et le jeu d'instruction. Dans le cas le plus simple, on a un séquenceur câblé secondé par un séquenceur microcodé, les deux étant précédés par un '''circuit de prédécodage'''. Le circuit de prédécodage reçoit les instructions et les redirige soit vers le séquenceur câblé, soit vers le séquenceur microcodé. Les instructions les plus simples sont dirigées vers le séquenceur câblé, alors que les instructions complexes vont vers le microcode (généralement les instructions avec des modes d'adressage exotiques). Une solution intéressante est de décoder les instructions qui prennent un seul cycle dans un séquenceur câblé, alors que les instructions multicycles sont décodées par un séquenceur microcodé séparé. Mais dans le cas général, la séparation en deux séquenceurs n'est pas évidente et on trouve un ''control store'' entouré de circuits câblés, avec certaines instructions qui n'ont pas besoin du microcode pour être décodées, d'autres qui passent par le microcode, d'autres qui sont décodé partiellement par microcode et partiellement par des circuits câblés. Notons que le microcode vertical n'est pas un séquenceur hybride, car toutes les instructions passent par le microcode. Par contre, un séquenceur hybride peut utiliser un microcode vertical, ce qui rend le séquenceur assez compliqué. Sur les processeurs x86 modernes, on trouve plusieurs séquenceurs : plusieurs décodeurs câblés spécialisés, et un microcode séparé. ===Le séquenceur hybride du 8086=== Un bon exemple de séquenceur de ce type est celui du processeur x86 8086 d'Intel, ainsi que ceux qui ont suivi. Le jeu d'instruction x86 est tellement complexe qu'il utilise un séquenceur hybride. Le séquenceur de l'Intel 8086 est organisé comme suit : un ''control store'' de 512 microinstructions (512 bytes) couplé à de nombreux circuit câblés, et une ''Group Decode ROM'' qui décide pour chaque instruction si elle est décodée par le séquenceur câblé ou le microcodé. La mal-nommée ''Group Decode ROM'' est en réalité un petit circuit combinatoire un peu particulier (basé sur un PAL, composant proche d'une ROM), qui commande le séquenceur proprement dit. Il fournit 15 signaux qui configurent le séquenceur et disent si le décodage doit utiliser ou non le microcode. Il permet aussi de configurer le microcode pour gérer les différents modes d'adressage, ou encore de configurer les circuits câblés en aval du microcode. Sur ce processeur, les instructions qui s’exécutent en un seul cycle d'horloge sont décodés sans utiliser le microcode. Le 8086 utilise une sorte de micro-code vertical pour commander l'ALU. Entre l'ALU et le microcode, on trouve un mini-décodeur, qui décode l'opération envoyée par le microcode en signaux de commande. C'est un simple circuit combinatoire (un PLA pour être précis). La raison est que l'ALU du 8086 est quelque peu complexe. Nous l'avions vu dans le chapitre sur les unités de calcul, l'ALU du 8086 est basée sur un additionneur à propagation de retenue, où deux portes logiques sont remplacées par une porte logique universelle. Il faut commander ces deux portes universelles pour obtenir l'opération voulue, ce qui demande pas mal de signaux de commande. Et pour économiser de la place dans le microcode, l'opération à faire est encodée sur plusieurs bits, qui sont décodés pour générer les signaux de commande. Ce qui vient d'être dit est une simplification. En vérité, certaines instructions ne sont pas encodées dans le microcode. Pour ces instructions, l'opération est récupérée directement dans l'opcode de l'instruction. C'est le cas des opérations d'addition, soustraction, les opérations logiques, les décalages et autres opérations gérées naturellement par l'ALU. Pour elles, l'opération à faire est extraite de l'opcode, pas du microcode. Pour ces opérations, le microcode encode l'opération à exécuter sur l'ALU par une micro-instruction généraliste nommée XI. La micro-opération XI indique qu'il faut activer le multiplexeur. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'unité de chargement et le program counter | prevText=L'unité de chargement et le program counter | next=L'implémentation matérielle des branchements | nextText=L'implémentation matérielle des branchements }} </noinclude> l4v3s9mf3t6sjbjks3w9o5wj4r09s6x Fonctionnement d'un ordinateur/Sommaire 0 69596 763751 762948 2026-04-16T13:27:23Z Mewtow 31375 /* La micro-architecture */ 763751 wikitext text/x-wiki __NOTOC__ * [[Fonctionnement d'un ordinateur/Introduction|Introduction]] ==Le codage des informations== * [[Fonctionnement d'un ordinateur/L'encodage des données|L'encodage des données]] * [[Fonctionnement d'un ordinateur/Le codage des nombres|Le codage des nombres]] * [[Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur|Les codes de détection/correction d'erreur]] ==Les circuits électroniques== * [[Fonctionnement d'un ordinateur/Les portes logiques|Les portes logiques]] ===Les circuits combinatoires=== * [[Fonctionnement d'un ordinateur/Les circuits combinatoires|Les circuits combinatoires]] * [[Fonctionnement d'un ordinateur/Les circuits de masquage|Les circuits de masquage]] * [[Fonctionnement d'un ordinateur/Les circuits de sélection|Les circuits de sélection]] ===Les circuits séquentiels=== * [[Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit|Les bascules : des mémoires de 1 bit]] * [[Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones|Les circuits synchrones et asynchrones]] * [[Fonctionnement d'un ordinateur/Les registres et mémoires adressables|Les registres et mémoires adressables]] * [[Fonctionnement d'un ordinateur/Les circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]] * [[Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence|Les timers et diviseurs de fréquence]] ===Les circuits de calcul et de comparaison=== * [[Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation|Les circuits de décalage et de rotation]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction|Les circuits pour l'addition et la soustraction]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)|Les unités arithmétiques et logiques entières (simples)]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande|Les circuits pour l'addition multiopérande]] * [[Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division|Les circuits pour la multiplication et la division]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] ===Les circuits intégrés à semi-conducteurs=== * [[Fonctionnement d'un ordinateur/Les transistors et portes logiques|Les transistors et portes logiques]] * [[Fonctionnement d'un ordinateur/Les circuits intégrés|Les circuits intégrés]] * [[Fonctionnement d'un ordinateur/L'interface électrique entre circuits intégrés et bus|L'interface électrique entre circuits intégrés et bus]] ==L'architecture d'un ordinateur== * [[Fonctionnement d'un ordinateur/L'architecture de base d'un ordinateur|L'architecture de base d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La hiérarchie mémoire|La hiérarchie mémoire]] * [[Fonctionnement d'un ordinateur/La performance d'un ordinateur|La performance d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La loi de Moore et les tendances technologiques|La loi de Moore et les tendances technologiques]] * [[Fonctionnement d'un ordinateur/Les techniques de réduction de la consommation électrique d'un processeur|Les techniques de réduction de la consommation électrique d'un processeur]] ==Les bus électroniques et la carte mère== * [[Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS|La carte mère, chipset et BIOS]] * [[Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)|Les bus et liaisons point à point (généralités)]] * [[Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus|Les encodages spécifiques aux bus]] * [[Fonctionnement d'un ordinateur/Les liaisons point à point|Les liaisons point à point]] * [[Fonctionnement d'un ordinateur/Les bus électroniques|Les bus électroniques]] * [[Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point|Quelques exemples de bus et de liaisons point à point]] ==Les mémoires RAM/ROM== * [[Fonctionnement d'un ordinateur/Les différents types de mémoires|Les différents types de mémoires]] * [[Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique|L'interface d'une mémoire électronique]] * [[Fonctionnement d'un ordinateur/Le bus mémoire|Le bus mémoire]] ===La micro-architecture d'une mémoire adressable=== * [[Fonctionnement d'un ordinateur/Les cellules mémoires|Les cellules mémoires]] * [[Fonctionnement d'un ordinateur/Le plan mémoire|Le plan mémoire]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire interne|Le contrôleur mémoire interne]] * [[Fonctionnement d'un ordinateur/Mémoires évoluées|Les mémoires évoluées]] ===Les mémoires primaires=== * [[Fonctionnement d'un ordinateur/Les mémoires ROM|Les mémoires ROM : Mask ROM, PROM, EPROM, EEPROM, Flash]] * [[Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones|Les mémoires SRAM synchrones]] * [[Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)|Les mémoires RAM dynamiques (DRAM)]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire externe|Le contrôleur mémoire externe]] ===Les mémoires exotiques=== * [[Fonctionnement d'un ordinateur/Les mémoires associatives|Les mémoires associatives]] * [[Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO|Les mémoires FIFO et LIFO]] ==Le processeur== ===L'architecture externe=== * [[Fonctionnement d'un ordinateur/Langage machine et assembleur|Langage machine et assembleur]] * [[Fonctionnement d'un ordinateur/Les registres du processeur|Les registres du processeur]] * [[Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme|Le modèle mémoire : alignement et boutisme]] * [[Fonctionnement d'un ordinateur/Les modes d'adressage|Les modes d'adressage]] * [[Fonctionnement d'un ordinateur/L'encodage des instructions|L'encodage des instructions]] * [[Fonctionnement d'un ordinateur/Les jeux d'instructions|Les jeux d'instructions]] * [[Fonctionnement d'un ordinateur/La pile d'appel et les fonctions|La pile d'appel et les fonctions]] * [[Fonctionnement d'un ordinateur/Les interruptions et exceptions|Les interruptions et exceptions]] ===La micro-architecture=== * [[Fonctionnement d'un ordinateur/Les composants d'un processeur|Les composants d'un processeur]] * [[Fonctionnement d'un ordinateur/Le chemin de données|Le chemin de données]] * [[Fonctionnement d'un ordinateur/L'unité de chargement et le program counter|L'unité de chargement et le program counter]] * [[Fonctionnement d'un ordinateur/L'unité de contrôle|L'unité de contrôle]] * [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements|L'implémentation matérielle des branchements]] ===Les jeux d'instruction spécialisés ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire|Les architectures à pile et mémoire-mémoire]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] * [[Fonctionnement d'un ordinateur/Les processeurs de traitement du signal|Les processeurs de traitement du signal]] * [[Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement|Les architectures actionnées par déplacement]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Compléments sur les mémoires de masse|Compléments sur les mémoires de masse]] ==La ou les mémoires caches== * [[Fonctionnement d'un ordinateur/Les mémoires cache|Les mémoires cache]] * [[Fonctionnement d'un ordinateur/Le préchargement|Le préchargement]] * [[Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer|Le ''Translation Lookaside Buffer'']] ==Le parallélisme d’instructions== * [[Fonctionnement d'un ordinateur/Le pipeline|Le pipeline]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/La prédiction de branchement|La prédiction de branchement]] * [[Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions|Les optimisations du chargement des instructions]] ===L’exécution dans le désordre=== * [[Fonctionnement d'un ordinateur/Les pipelines multicycles|Les pipelines multicycles]] * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Le contournement (data forwarding)|Le contournement (data forwarding)]] * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Annexe : Le scoreboarding et l'algorithme de Tomasulo]] * [[Fonctionnement d'un ordinateur/La désambiguïsation mémoire|La désambiguïsation mémoire]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache|Le parallélisme mémoire au niveau du cache]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86|Exemples de CPU superscalaires: le cas du x86]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== * [[Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO|Les coprocesseurs : FPU et IO]] * [[Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation|L'accélération matérielle de la virtualisation]] * [[Fonctionnement d'un ordinateur/Les ISA optimisés pour la compilation/interprétation|Les ISA optimisés pour la compilation/interprétation]] * [[Fonctionnement d'un ordinateur/Le matériel réseau|Le matériel réseau]] * [[Fonctionnement d'un ordinateur/La tolérance aux pannes|La tolérance aux pannes]] * [[Fonctionnement d'un ordinateur/Les architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] * [[Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires|Les ordinateurs de première génération : tubes à vide et mémoires]] * [[Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires|Les ordinateurs à encodages non-binaires]] * [[Fonctionnement d'un ordinateur/Les circuits réversibles|Les circuits réversibles]] {{autocat}} gk6riwaoosjll430i3q5hx4tx4cfuhq 763771 763751 2026-04-16T14:11:24Z Mewtow 31375 /* Annexes */ 763771 wikitext text/x-wiki __NOTOC__ * [[Fonctionnement d'un ordinateur/Introduction|Introduction]] ==Le codage des informations== * [[Fonctionnement d'un ordinateur/L'encodage des données|L'encodage des données]] * [[Fonctionnement d'un ordinateur/Le codage des nombres|Le codage des nombres]] * [[Fonctionnement d'un ordinateur/Les codes de détection/correction d'erreur|Les codes de détection/correction d'erreur]] ==Les circuits électroniques== * [[Fonctionnement d'un ordinateur/Les portes logiques|Les portes logiques]] ===Les circuits combinatoires=== * [[Fonctionnement d'un ordinateur/Les circuits combinatoires|Les circuits combinatoires]] * [[Fonctionnement d'un ordinateur/Les circuits de masquage|Les circuits de masquage]] * [[Fonctionnement d'un ordinateur/Les circuits de sélection|Les circuits de sélection]] ===Les circuits séquentiels=== * [[Fonctionnement d'un ordinateur/Les bascules : des mémoires de 1 bit|Les bascules : des mémoires de 1 bit]] * [[Fonctionnement d'un ordinateur/Les circuits synchrones et asynchrones|Les circuits synchrones et asynchrones]] * [[Fonctionnement d'un ordinateur/Les registres et mémoires adressables|Les registres et mémoires adressables]] * [[Fonctionnement d'un ordinateur/Les circuits compteurs et décompteurs|Les circuits compteurs et décompteurs]] * [[Fonctionnement d'un ordinateur/Les timers et diviseurs de fréquence|Les timers et diviseurs de fréquence]] ===Les circuits de calcul et de comparaison=== * [[Fonctionnement d'un ordinateur/Les circuits de décalage et de rotation|Les circuits de décalage et de rotation]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition et la soustraction|Les circuits pour l'addition et la soustraction]] * [[Fonctionnement d'un ordinateur/Les circuits de comparaison|Les circuits de comparaison]] * [[Fonctionnement d'un ordinateur/Les unités arithmétiques et logiques entières (simples)|Les unités arithmétiques et logiques entières (simples)]] * [[Fonctionnement d'un ordinateur/Les circuits pour l'addition multiopérande|Les circuits pour l'addition multiopérande]] * [[Fonctionnement d'un ordinateur/Les circuits pour la multiplication et la division|Les circuits pour la multiplication et la division]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul logique et bit à bit|Les circuits de calcul logique et bit à bit]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul flottant|Les circuits de calcul flottant]] * [[Fonctionnement d'un ordinateur/Les circuits de calcul trigonométriques|Les circuits de calcul trigonométriques]] * [[Fonctionnement d'un ordinateur/Les circuits de conversion analogique-numérique|Les circuits de conversion analogique-numérique]] ===Les circuits intégrés à semi-conducteurs=== * [[Fonctionnement d'un ordinateur/Les transistors et portes logiques|Les transistors et portes logiques]] * [[Fonctionnement d'un ordinateur/Les circuits intégrés|Les circuits intégrés]] * [[Fonctionnement d'un ordinateur/L'interface électrique entre circuits intégrés et bus|L'interface électrique entre circuits intégrés et bus]] ==L'architecture d'un ordinateur== * [[Fonctionnement d'un ordinateur/L'architecture de base d'un ordinateur|L'architecture de base d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La hiérarchie mémoire|La hiérarchie mémoire]] * [[Fonctionnement d'un ordinateur/La performance d'un ordinateur|La performance d'un ordinateur]] * [[Fonctionnement d'un ordinateur/La loi de Moore et les tendances technologiques|La loi de Moore et les tendances technologiques]] * [[Fonctionnement d'un ordinateur/Les techniques de réduction de la consommation électrique d'un processeur|Les techniques de réduction de la consommation électrique d'un processeur]] ==Les bus électroniques et la carte mère== * [[Fonctionnement d'un ordinateur/La carte mère, chipset et BIOS|La carte mère, chipset et BIOS]] * [[Fonctionnement d'un ordinateur/Les bus et liaisons point à point (généralités)|Les bus et liaisons point à point (généralités)]] * [[Fonctionnement d'un ordinateur/Les encodages spécifiques aux bus|Les encodages spécifiques aux bus]] * [[Fonctionnement d'un ordinateur/Les liaisons point à point|Les liaisons point à point]] * [[Fonctionnement d'un ordinateur/Les bus électroniques|Les bus électroniques]] * [[Fonctionnement d'un ordinateur/Quelques exemples de bus et de liaisons point à point|Quelques exemples de bus et de liaisons point à point]] ==Les mémoires RAM/ROM== * [[Fonctionnement d'un ordinateur/Les différents types de mémoires|Les différents types de mémoires]] * [[Fonctionnement d'un ordinateur/L'interface d'une mémoire électronique|L'interface d'une mémoire électronique]] * [[Fonctionnement d'un ordinateur/Le bus mémoire|Le bus mémoire]] ===La micro-architecture d'une mémoire adressable=== * [[Fonctionnement d'un ordinateur/Les cellules mémoires|Les cellules mémoires]] * [[Fonctionnement d'un ordinateur/Le plan mémoire|Le plan mémoire]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire interne|Le contrôleur mémoire interne]] * [[Fonctionnement d'un ordinateur/Mémoires évoluées|Les mémoires évoluées]] ===Les mémoires primaires=== * [[Fonctionnement d'un ordinateur/Les mémoires ROM|Les mémoires ROM : Mask ROM, PROM, EPROM, EEPROM, Flash]] * [[Fonctionnement d'un ordinateur/Les mémoires SRAM synchrones|Les mémoires SRAM synchrones]] * [[Fonctionnement d'un ordinateur/Les mémoires RAM dynamiques (DRAM)|Les mémoires RAM dynamiques (DRAM)]] * [[Fonctionnement d'un ordinateur/Contrôleur mémoire externe|Le contrôleur mémoire externe]] ===Les mémoires exotiques=== * [[Fonctionnement d'un ordinateur/Les mémoires associatives|Les mémoires associatives]] * [[Fonctionnement d'un ordinateur/Les mémoires FIFO et LIFO|Les mémoires FIFO et LIFO]] ==Le processeur== ===L'architecture externe=== * [[Fonctionnement d'un ordinateur/Langage machine et assembleur|Langage machine et assembleur]] * [[Fonctionnement d'un ordinateur/Les registres du processeur|Les registres du processeur]] * [[Fonctionnement d'un ordinateur/Le modèle mémoire : alignement et boutisme|Le modèle mémoire : alignement et boutisme]] * [[Fonctionnement d'un ordinateur/Les modes d'adressage|Les modes d'adressage]] * [[Fonctionnement d'un ordinateur/L'encodage des instructions|L'encodage des instructions]] * [[Fonctionnement d'un ordinateur/Les jeux d'instructions|Les jeux d'instructions]] * [[Fonctionnement d'un ordinateur/La pile d'appel et les fonctions|La pile d'appel et les fonctions]] * [[Fonctionnement d'un ordinateur/Les interruptions et exceptions|Les interruptions et exceptions]] ===La micro-architecture=== * [[Fonctionnement d'un ordinateur/Les composants d'un processeur|Les composants d'un processeur]] * [[Fonctionnement d'un ordinateur/Le chemin de données|Le chemin de données]] * [[Fonctionnement d'un ordinateur/L'unité de chargement et le program counter|L'unité de chargement et le program counter]] * [[Fonctionnement d'un ordinateur/L'unité de contrôle|L'unité de contrôle]] * [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements|L'implémentation matérielle des branchements]] ===Les jeux d'instruction spécialisés ou exotiques=== * [[Fonctionnement d'un ordinateur/Les architectures à accumulateur|Les architectures à accumulateur]] * [[Fonctionnement d'un ordinateur/Les architectures à pile et mémoire-mémoire|Les architectures à pile et mémoire-mémoire]] * [[Fonctionnement d'un ordinateur/Les processeurs 8 bits et moins|Les processeurs 8 bits et moins]] * [[Fonctionnement d'un ordinateur/Les processeurs de traitement du signal|Les processeurs de traitement du signal]] * [[Fonctionnement d'un ordinateur/Les architectures actionnées par déplacement|Les architectures actionnées par déplacement]] ===L'espace d'adressage du processeur et la multiprogrammation=== * [[Fonctionnement d'un ordinateur/L'espace d'adressage du processeur|L'espace d'adressage du processeur]] * [[Fonctionnement d'un ordinateur/L'abstraction mémoire et la mémoire virtuelle|L'abstraction mémoire et la mémoire virtuelle]] ==Les entrées-sorties et périphériques== * [[Fonctionnement d'un ordinateur/Les méthodes de synchronisation entre processeur et périphériques|Les méthodes de synchronisation entre processeur et périphériques]] * [[Fonctionnement d'un ordinateur/L'adressage des périphériques|L'adressage des périphériques]] * [[Fonctionnement d'un ordinateur/Les périphériques et les cartes d'extension|Les périphériques et les cartes d'extension]] ==Les mémoires de stockage== * [[Fonctionnement d'un ordinateur/Les mémoires de masse : généralités|Les mémoires de masse : généralités]] * [[Fonctionnement d'un ordinateur/Les disques durs|Les disques durs]] * [[Fonctionnement d'un ordinateur/Les solid-state drives|Les solid-state drives]] * [[Fonctionnement d'un ordinateur/Les disques optiques|Les disques optiques]] * [[Fonctionnement d'un ordinateur/Compléments sur les mémoires de masse|Compléments sur les mémoires de masse]] ==La ou les mémoires caches== * [[Fonctionnement d'un ordinateur/Les mémoires cache|Les mémoires cache]] * [[Fonctionnement d'un ordinateur/Le préchargement|Le préchargement]] * [[Fonctionnement d'un ordinateur/Le Translation Lookaside Buffer|Le ''Translation Lookaside Buffer'']] ==Le parallélisme d’instructions== * [[Fonctionnement d'un ordinateur/Le pipeline|Le pipeline]] ===Les branchements et le ''front-end''=== * [[Fonctionnement d'un ordinateur/La prédiction de branchement|La prédiction de branchement]] * [[Fonctionnement d'un ordinateur/Les optimisations du chargement des instructions|Les optimisations du chargement des instructions]] ===L’exécution dans le désordre=== * [[Fonctionnement d'un ordinateur/Les pipelines multicycles|Les pipelines multicycles]] * [[Fonctionnement d'un ordinateur/L'émission dans l'ordre des instructions|L'émission dans l'ordre des instructions]] * [[Fonctionnement d'un ordinateur/Le contournement (data forwarding)|Le contournement (data forwarding)]] * [[Fonctionnement d'un ordinateur/L'exécution dans le désordre|L'exécution dans le désordre]] * [[Fonctionnement d'un ordinateur/Le renommage de registres|Le renommage de registres]] * [[Fonctionnement d'un ordinateur/Le scoreboarding et l'algorithme de Tomasulo|Annexe : Le scoreboarding et l'algorithme de Tomasulo]] * [[Fonctionnement d'un ordinateur/La désambiguïsation mémoire|La désambiguïsation mémoire]] * [[Fonctionnement d'un ordinateur/Le parallélisme mémoire au niveau du cache|Le parallélisme mémoire au niveau du cache]] ===L'émission multiple=== * [[Fonctionnement d'un ordinateur/Les processeurs superscalaires|Les processeurs superscalaires]] * [[Fonctionnement d'un ordinateur/Exemples de microarchitectures CPU : le cas du x86|Exemples de CPU superscalaires: le cas du x86]] * [[Fonctionnement d'un ordinateur/Les processeurs VLIW et EPIC|Les processeurs VLIW et EPIC]] * [[Fonctionnement d'un ordinateur/Les architectures dataflow|Les architectures dataflow]] ==Les architectures parallèles== * [[Fonctionnement d'un ordinateur/Les architectures parallèles|Les architectures parallèles]] * [[Fonctionnement d'un ordinateur/Architectures multiprocesseurs et multicœurs|Les architectures multiprocesseurs et multicœurs]] * [[Fonctionnement d'un ordinateur/Architectures multithreadées et Hyperthreading|Les architectures multithreadées et Hyperthreading]] * [[Fonctionnement d'un ordinateur/Les architectures à parallélisme de données|Les architectures à parallélisme de données]] * [[Fonctionnement d'un ordinateur/La cohérence des caches|La cohérence des caches]] * [[Fonctionnement d'un ordinateur/Les sections critiques et le modèle mémoire|Les sections critiques et le modèle mémoire]] ==Annexes== ===Annexes sur les nombres flottants et les FPUs=== * [[Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87|Un exemple de jeu d'instruction : l'extension x87]] * [[Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO|Les coprocesseurs : FPU et IO]] ===Autres annexes=== * [[Fonctionnement d'un ordinateur/L'accélération matérielle de la virtualisation|L'accélération matérielle de la virtualisation]] * [[Fonctionnement d'un ordinateur/Les ISA optimisés pour la compilation/interprétation|Les ISA optimisés pour la compilation/interprétation]] * [[Fonctionnement d'un ordinateur/Le matériel réseau|Le matériel réseau]] * [[Fonctionnement d'un ordinateur/La tolérance aux pannes|La tolérance aux pannes]] * [[Fonctionnement d'un ordinateur/Les architectures systoliques|Les architectures systoliques]] * [[Fonctionnement d'un ordinateur/Les architectures neuromorphiques|Les réseaux de neurones matériels]] * [[Fonctionnement d'un ordinateur/Les ordinateurs de première génération : tubes à vide et mémoires|Les ordinateurs de première génération : tubes à vide et mémoires]] * [[Fonctionnement d'un ordinateur/Les ordinateurs à encodages non-binaires|Les ordinateurs à encodages non-binaires]] * [[Fonctionnement d'un ordinateur/Les circuits réversibles|Les circuits réversibles]] {{autocat}} 4prep2ewh8n18zqa9bvpuuvoestzs3o Les cartes graphiques/Sommaire 0 70681 763810 763531 2026-04-16T20:20:59Z Mewtow 31375 /* Le pipeline fixe, non-programmable */ 763810 wikitext text/x-wiki * [[Les cartes graphiques/Les cartes d'affichage|Introduction : les cartes d’affichage]] ===Les cartes d'affichage et d'accélération 2D=== * [[Les cartes graphiques/Le Video Display Controler|Le Video Display Controler]] * [[Les cartes graphiques/Les systèmes à framebuffer|Les systèmes à framebuffer]] * [[Les cartes graphiques/Les cartes accélératrices 2D|Les cartes accélératrices 2D]] * [[Les cartes graphiques/Le mode texte et le rendu en tiles|Le mode texte et le rendu en tiles]] * [[Les cartes graphiques/Les accélérateurs de scanline|Les accélérateurs de scanline]] * [[Les cartes graphiques/Les Video Display Controler atypiques|Les Video Display Controler atypiques]] * [[Les cartes graphiques/Les cartes d'affichage des anciens PC|Les cartes d'affichage des anciens PC]] ===Les cartes accélératrices 3D=== * [[Les cartes graphiques/Le rendu d'une scène 3D : concepts de base|Le rendu d'une scène 3D : concepts de base]] * [[Les cartes graphiques/Avant les GPUs : les cartes accélératrices 3D|Avant les GPUs : les cartes accélératrices 3D]] * [[Les cartes graphiques/L'évolution vers la programmabilité : les GPUs|L'évolution vers la programmabilité : les GPUs]] ===Les processeurs de ''shader''=== * [[Les cartes graphiques/Les processeurs de shaders|Les processeurs de shaders]] * [[Les cartes graphiques/La microarchitecture des processeurs de shaders|La microarchitecture des processeurs de shaders]] * [[Les cartes graphiques/Les processeurs de shader VLIW et DirectX 9|Les processeurs de shader VLIW et DirectX 9]] * [[Les cartes graphiques/Les caches d'un processeur de shader|Les caches d'un processeur de shader]] ===La mémoire vidéo (VRAM)=== * [[Les cartes graphiques/La mémoire unifiée et la mémoire vidéo dédiée|La mémoire unifiée et la mémoire vidéo dédiée]] ===Le processeur de commande=== * [[Les cartes graphiques/Le rendu d'une scène 3D : l'API graphique|Le rendu d'une scène 3D : l'API graphique]] * [[Les cartes graphiques/Le processeur de commandes|Le processeur de commandes]] * [[Les cartes graphiques/La répartition du travail sur les unités de shaders|La répartition du travail sur les unités de shaders]] ===Le pipeline fixe, non-programmable=== * [[Les cartes graphiques/Le pipeline géométrique : évolution|Le pipeline géométrique : évolution]] * [[Les cartes graphiques/Le pipeline géométrique d'un GPU|Le pipeline géométrique d'un GPU]] * [[Les cartes graphiques/Le rasterizeur|Le rasterizeur]] * [[Les cartes graphiques/Les unités de texture|Les unités de texture]] * [[Les cartes graphiques/Les Render Output Target|Les Render Output Target]] * [[Les cartes graphiques/Les écritures en VRAM hors ROPs|Les écritures en VRAM hors ROPs]] ===Annexe=== * [[Les cartes graphiques/Le support matériel du lancer de rayons|Le support matériel du lancer de rayons]] * [[Les cartes graphiques/L'antialiasing|L'antialiasing]] * [[Les cartes graphiques/Le multi-GPU|Le multi-GPU]] {{autocat}} ababtvmoglqltxglzb0dp8daf0v7l5l Fonctionnement d'un ordinateur/Un exemple de jeu d'instruction : l'extension x87 0 71280 763760 740623 2026-04-16T13:46:44Z Mewtow 31375 763760 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'une architecture qui a reçu de nombreux ajouts au cours du temps, et qui a été fortement modifiée, tout en gardant une certaine compatibilité avec les versions plus anciennes. Ce qui a donné un jeu d’instruction particulièrement compliqué. Dans ce chapitre, nous allons étudier un de ces ajout, une extension de ce jeu d'instruction, qui a ajouté la gestion des flottants au x86. La gestion des flottants est une option qui a été rajoutée au cours de l'existence de l'architecture. Si les processeurs x86 des années 80 ne pouvaient pas faire des calculs flottants, ou alors seulement avec l'aide d'un coprocesseur, tous les processeurs x86 actuels le peuvent. L'ensemble des instructions machine x86 liées aux flottants s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== L'extension x87 est un cas d'architecture à pile, assez spécialisée, totalement différente de la gestion des registres x86 normaux. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 : le premier registre est le registre 7, tandis que le dernier registre est le registre 0. Lorsque la FPU x87 est initialisée, ces 8 registres sont complètement vides : ils ne contiennent aucun flottant. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'une opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à une opérande (les instructions de calcul d'une tangente, d'une racine carrée et d'autres) vont dépiler le flottant au sommet de la pile. Les instructions à deux opérandes (multiplication, addition, soustraction et autres) peuvent se comporter de plusieurs manières différentes. Le cas le plus simple est celui attendu de la part d'une architecture à pile : l'instruction dépile les deux opérandes au sommet de la pile. La seconde possibilité est celle attendue de la part d'une architecture à pile à une adresse : elles dépilent le sommet de la pile et chargent l'autre opérande depuis la mémoire RAM. Le troisième cas est plus intéressant : l'instruction dépile le sommet de la pile, et charge l'autre opérande depuis n'importe quel autre registre de la pile. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Si vous avez bonne mémoire, vous vous souvenez sûrement de ce que j'ai dit que la FPU contient 3 registres spéciaux qui ne stockent pas de flottants, mais sont malgré tout utiles. Ces 3 registres portent les noms de ''Control Word'', ''Status Word'' et ''Tag Word''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Ce registre contient 16 bits et pour chacun des 8 registres de données de la FPU, 2 bits sont réservés dans le registre Tag Word. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Passons maintenant au ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. {|class="wikitable" |- !Bit !Utilité |- |TOP |Ce registre contient trois bits regroupés en un seul ensemble nommé TOP, qui stocke le numéro du premier registre vide dans l'ordre de remplissage. Idéal pour gérer notre pile de registres |- |U |Sert à détecter les underflow. Il est mis à 1 lorsqu'un underflow a lieu. |- |O |Pareil que U, mais pour les overflow : ce registre est mis à 1 lors d'un overflow |- |Z |C'est un bit qui est mis à 1 lorsque notre FPU exécute une division par zéro |- |D |Ce bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- |I |Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Enfin, voyons le ''Control Word'', le petit dernier. Il fait 16 bits et contient lui aussi des bits ayant chacun une utilité précise. Beaucoup de bits de ce registre sont inutilisés et on ne va citer que les plus utiles. {|class="wikitable" |- !Bit !Utilité |- |Infinity Control |S'il vaut zéro, les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. S'il vaut un, les infinis sont traités normalement |- |Rouding Control |C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- |Precision Control |Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ==Les instructions flottantes x87== L’extension x87 comprend les instructions de base supportées par la norme IEEE 754, ainsi que quelques autres. On y retrouve les quatre instructions arithmétiques de base de la norme IEEE754 (+, -, *, /), avec quelques autres calculs supplémentaires. ===Les comparaisons=== Voici une liste de quelques instructions de comparaisons supportées par l'extension x87 : * FTST : compare le sommet de la pseudo-pile avec la valeur 0 ; * FICOM : compare le contenu du sommet de la pseudo-pile avec une constante entière ; * FCOM : compare le contenu du sommet de la pseudo-pile avec une constante flottante ; * FCOMI : compare le contenu des deux flottants au sommet de la pseudo-pile. ===Les instructions arithmétiques=== On trouve aussi des instructions de calculs, qui comprennent les cinq opérations définies par la norme IEE754, mais aussi quelques instructions supplémentaires : * l'addition : FADD ; * la soustraction FSUB ; * la multiplication FMUL ; * la division FDIV ; * la racine carrée FSQRT ; * des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). L'extension x87 implémente aussi des instructions trigonométriques et analytiques telles que : * le cosinus : instruction FCOS ; * le sinus : instruction FSIN ; * la tangente : instruction FPTAN ; * l'arc tangente : instruction FPATAN ; * ou encore des instructions de calcul de logarithmes ou d'exponentielles. Il va de soi que ces dernières ne sont pas supportées par la norme IEEE 754 et que tout compilateur qui souhaite être compatible avec la norme IEEE 754 ne doit pas les utiliser. ===Les instructions d'accès mémoire=== En plus de ces instructions de calcul, l'extension x87 fournit des instructions pour transférer des flottants entre la mémoire et les registres. Celles-ci sont des équivalents des instructions PUSH et POP qu'on trouve sur les machines à pile, à l'exception d'une instruction équivalente à l'instruction SWAP. On peut citer par exemple les instructions dans le tableau suivant. D'autres instructions chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. {|class="wikitable" |- !Instruction !Ce qu'elle fait |- !FLD |Elle est capable de charger un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- !FSTP |Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- !FXCH |Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> awetnu2c7ees1uaw50h1jvrfp8k7t0x 763761 763760 2026-04-16T13:48:16Z Mewtow 31375 763761 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE. Dans ce chapitre, nous allons étudier un de ces ajout, une extension de ce jeu d'instruction, qui a ajouté la gestion des flottants au x86. La gestion des flottants est une option qui a été rajoutée au cours de l'existence de l'architecture. Si les processeurs x86 des années 80 ne pouvaient pas faire des calculs flottants, ou alors seulement avec l'aide d'un coprocesseur, tous les processeurs x86 actuels le peuvent. L'ensemble des instructions machine x86 liées aux flottants s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== L'extension x87 est un cas d'architecture à pile, assez spécialisée, totalement différente de la gestion des registres x86 normaux. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 : le premier registre est le registre 7, tandis que le dernier registre est le registre 0. Lorsque la FPU x87 est initialisée, ces 8 registres sont complètement vides : ils ne contiennent aucun flottant. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'une opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à une opérande (les instructions de calcul d'une tangente, d'une racine carrée et d'autres) vont dépiler le flottant au sommet de la pile. Les instructions à deux opérandes (multiplication, addition, soustraction et autres) peuvent se comporter de plusieurs manières différentes. Le cas le plus simple est celui attendu de la part d'une architecture à pile : l'instruction dépile les deux opérandes au sommet de la pile. La seconde possibilité est celle attendue de la part d'une architecture à pile à une adresse : elles dépilent le sommet de la pile et chargent l'autre opérande depuis la mémoire RAM. Le troisième cas est plus intéressant : l'instruction dépile le sommet de la pile, et charge l'autre opérande depuis n'importe quel autre registre de la pile. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Si vous avez bonne mémoire, vous vous souvenez sûrement de ce que j'ai dit que la FPU contient 3 registres spéciaux qui ne stockent pas de flottants, mais sont malgré tout utiles. Ces 3 registres portent les noms de ''Control Word'', ''Status Word'' et ''Tag Word''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Ce registre contient 16 bits et pour chacun des 8 registres de données de la FPU, 2 bits sont réservés dans le registre Tag Word. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Passons maintenant au ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. {|class="wikitable" |- !Bit !Utilité |- |TOP |Ce registre contient trois bits regroupés en un seul ensemble nommé TOP, qui stocke le numéro du premier registre vide dans l'ordre de remplissage. Idéal pour gérer notre pile de registres |- |U |Sert à détecter les underflow. Il est mis à 1 lorsqu'un underflow a lieu. |- |O |Pareil que U, mais pour les overflow : ce registre est mis à 1 lors d'un overflow |- |Z |C'est un bit qui est mis à 1 lorsque notre FPU exécute une division par zéro |- |D |Ce bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- |I |Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Enfin, voyons le ''Control Word'', le petit dernier. Il fait 16 bits et contient lui aussi des bits ayant chacun une utilité précise. Beaucoup de bits de ce registre sont inutilisés et on ne va citer que les plus utiles. {|class="wikitable" |- !Bit !Utilité |- |Infinity Control |S'il vaut zéro, les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. S'il vaut un, les infinis sont traités normalement |- |Rouding Control |C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- |Precision Control |Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ==Les instructions flottantes x87== L’extension x87 comprend les instructions de base supportées par la norme IEEE 754, ainsi que quelques autres. On y retrouve les quatre instructions arithmétiques de base de la norme IEEE754 (+, -, *, /), avec quelques autres calculs supplémentaires. ===Les comparaisons=== Voici une liste de quelques instructions de comparaisons supportées par l'extension x87 : * FTST : compare le sommet de la pseudo-pile avec la valeur 0 ; * FICOM : compare le contenu du sommet de la pseudo-pile avec une constante entière ; * FCOM : compare le contenu du sommet de la pseudo-pile avec une constante flottante ; * FCOMI : compare le contenu des deux flottants au sommet de la pseudo-pile. ===Les instructions arithmétiques=== On trouve aussi des instructions de calculs, qui comprennent les cinq opérations définies par la norme IEE754, mais aussi quelques instructions supplémentaires : * l'addition : FADD ; * la soustraction FSUB ; * la multiplication FMUL ; * la division FDIV ; * la racine carrée FSQRT ; * des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). L'extension x87 implémente aussi des instructions trigonométriques et analytiques telles que : * le cosinus : instruction FCOS ; * le sinus : instruction FSIN ; * la tangente : instruction FPTAN ; * l'arc tangente : instruction FPATAN ; * ou encore des instructions de calcul de logarithmes ou d'exponentielles. Il va de soi que ces dernières ne sont pas supportées par la norme IEEE 754 et que tout compilateur qui souhaite être compatible avec la norme IEEE 754 ne doit pas les utiliser. ===Les instructions d'accès mémoire=== En plus de ces instructions de calcul, l'extension x87 fournit des instructions pour transférer des flottants entre la mémoire et les registres. Celles-ci sont des équivalents des instructions PUSH et POP qu'on trouve sur les machines à pile, à l'exception d'une instruction équivalente à l'instruction SWAP. On peut citer par exemple les instructions dans le tableau suivant. D'autres instructions chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. {|class="wikitable" |- !Instruction !Ce qu'elle fait |- !FLD |Elle est capable de charger un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- !FSTP |Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- !FXCH |Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> f0rtdpcmcr24ugbqhyesau5dvkge6x8 763762 763761 2026-04-16T13:49:36Z Mewtow 31375 763762 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== L'extension x87 est un cas d'architecture à pile, assez spécialisée, totalement différente de la gestion des registres x86 normaux. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 : le premier registre est le registre 7, tandis que le dernier registre est le registre 0. Lorsque la FPU x87 est initialisée, ces 8 registres sont complètement vides : ils ne contiennent aucun flottant. Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'une opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4e registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à une opérande (les instructions de calcul d'une tangente, d'une racine carrée et d'autres) vont dépiler le flottant au sommet de la pile. Les instructions à deux opérandes (multiplication, addition, soustraction et autres) peuvent se comporter de plusieurs manières différentes. Le cas le plus simple est celui attendu de la part d'une architecture à pile : l'instruction dépile les deux opérandes au sommet de la pile. La seconde possibilité est celle attendue de la part d'une architecture à pile à une adresse : elles dépilent le sommet de la pile et chargent l'autre opérande depuis la mémoire RAM. Le troisième cas est plus intéressant : l'instruction dépile le sommet de la pile, et charge l'autre opérande depuis n'importe quel autre registre de la pile. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Si vous avez bonne mémoire, vous vous souvenez sûrement de ce que j'ai dit que la FPU contient 3 registres spéciaux qui ne stockent pas de flottants, mais sont malgré tout utiles. Ces 3 registres portent les noms de ''Control Word'', ''Status Word'' et ''Tag Word''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Ce registre contient 16 bits et pour chacun des 8 registres de données de la FPU, 2 bits sont réservés dans le registre Tag Word. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Passons maintenant au ''Status Word''. Celui-ci fait lui aussi 16 bits et contient tout ce qu'il faut pour qu'un programme puisse comprendre la cause d'une exception. {|class="wikitable" |- !Bit !Utilité |- |TOP |Ce registre contient trois bits regroupés en un seul ensemble nommé TOP, qui stocke le numéro du premier registre vide dans l'ordre de remplissage. Idéal pour gérer notre pile de registres |- |U |Sert à détecter les underflow. Il est mis à 1 lorsqu'un underflow a lieu. |- |O |Pareil que U, mais pour les overflow : ce registre est mis à 1 lors d'un overflow |- |Z |C'est un bit qui est mis à 1 lorsque notre FPU exécute une division par zéro |- |D |Ce bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- |I |Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Enfin, voyons le ''Control Word'', le petit dernier. Il fait 16 bits et contient lui aussi des bits ayant chacun une utilité précise. Beaucoup de bits de ce registre sont inutilisés et on ne va citer que les plus utiles. {|class="wikitable" |- !Bit !Utilité |- |Infinity Control |S'il vaut zéro, les infinis sont tous traités comme s'ils valaient <math>+ \infty</math>. S'il vaut un, les infinis sont traités normalement |- |Rouding Control |C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- |Precision Control |Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} ==Les instructions flottantes x87== L’extension x87 comprend les instructions de base supportées par la norme IEEE 754, ainsi que quelques autres. On y retrouve les quatre instructions arithmétiques de base de la norme IEEE754 (+, -, *, /), avec quelques autres calculs supplémentaires. ===Les comparaisons=== Voici une liste de quelques instructions de comparaisons supportées par l'extension x87 : * FTST : compare le sommet de la pseudo-pile avec la valeur 0 ; * FICOM : compare le contenu du sommet de la pseudo-pile avec une constante entière ; * FCOM : compare le contenu du sommet de la pseudo-pile avec une constante flottante ; * FCOMI : compare le contenu des deux flottants au sommet de la pseudo-pile. ===Les instructions arithmétiques=== On trouve aussi des instructions de calculs, qui comprennent les cinq opérations définies par la norme IEE754, mais aussi quelques instructions supplémentaires : * l'addition : FADD ; * la soustraction FSUB ; * la multiplication FMUL ; * la division FDIV ; * la racine carrée FSQRT ; * des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). L'extension x87 implémente aussi des instructions trigonométriques et analytiques telles que : * le cosinus : instruction FCOS ; * le sinus : instruction FSIN ; * la tangente : instruction FPTAN ; * l'arc tangente : instruction FPATAN ; * ou encore des instructions de calcul de logarithmes ou d'exponentielles. Il va de soi que ces dernières ne sont pas supportées par la norme IEEE 754 et que tout compilateur qui souhaite être compatible avec la norme IEEE 754 ne doit pas les utiliser. ===Les instructions d'accès mémoire=== En plus de ces instructions de calcul, l'extension x87 fournit des instructions pour transférer des flottants entre la mémoire et les registres. Celles-ci sont des équivalents des instructions PUSH et POP qu'on trouve sur les machines à pile, à l'exception d'une instruction équivalente à l'instruction SWAP. On peut citer par exemple les instructions dans le tableau suivant. D'autres instructions chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. {|class="wikitable" |- !Instruction !Ce qu'elle fait |- !FLD |Elle est capable de charger un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- !FSTP |Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- !FXCH |Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> bktmy69wtn9yy38b0v8lqibesebrx6r 763764 763762 2026-04-16T13:55:28Z Mewtow 31375 /* Les registres x87 */ 763764 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== L’extension x87 comprend les instructions de base supportées par la norme IEEE 754, ainsi que quelques autres. On y retrouve les quatre instructions arithmétiques de base de la norme IEEE754 (+, -, *, /), avec quelques autres calculs supplémentaires. ===Les comparaisons=== Voici une liste de quelques instructions de comparaisons supportées par l'extension x87 : * FTST : compare le sommet de la pseudo-pile avec la valeur 0 ; * FICOM : compare le contenu du sommet de la pseudo-pile avec une constante entière ; * FCOM : compare le contenu du sommet de la pseudo-pile avec une constante flottante ; * FCOMI : compare le contenu des deux flottants au sommet de la pseudo-pile. ===Les instructions arithmétiques=== On trouve aussi des instructions de calculs, qui comprennent les cinq opérations définies par la norme IEE754, mais aussi quelques instructions supplémentaires : * l'addition : FADD ; * la soustraction FSUB ; * la multiplication FMUL ; * la division FDIV ; * la racine carrée FSQRT ; * des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). L'extension x87 implémente aussi des instructions trigonométriques et analytiques telles que : * le cosinus : instruction FCOS ; * le sinus : instruction FSIN ; * la tangente : instruction FPTAN ; * l'arc tangente : instruction FPATAN ; * ou encore des instructions de calcul de logarithmes ou d'exponentielles. Il va de soi que ces dernières ne sont pas supportées par la norme IEEE 754 et que tout compilateur qui souhaite être compatible avec la norme IEEE 754 ne doit pas les utiliser. ===Les instructions d'accès mémoire=== En plus de ces instructions de calcul, l'extension x87 fournit des instructions pour transférer des flottants entre la mémoire et les registres. Celles-ci sont des équivalents des instructions PUSH et POP qu'on trouve sur les machines à pile, à l'exception d'une instruction équivalente à l'instruction SWAP. On peut citer par exemple les instructions dans le tableau suivant. D'autres instructions chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. {|class="wikitable" |- !Instruction !Ce qu'elle fait |- !FLD |Elle est capable de charger un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- !FSTP |Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- !FXCH |Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> tq5pkyl7js155qh98bo7kisdz3zqf9a 763765 763764 2026-04-16T13:59:03Z Mewtow 31375 /* Les instructions flottantes x87 */ 763765 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> awt12seaarht58jzjf81hgd9gg5vyjj 763766 763765 2026-04-16T13:59:45Z Mewtow 31375 /* Les instructions flottantes x87 */ 763766 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Chacun des registres de données vus plus haut stocke un nombre flottant codé sur 80 bits. Oui, vous avez bien lu, 80 bits et non 32 ou 64 : cette FPU calcule sur des nombres flottants double précision étendue et non sur des flottants simple ou double précision, qui ne sont pas gérés par la FPU x87. On peut alors se demander comment le processeur fait pour calculer avec des flottants simple et double précision. Tout se joue lors de l'accès à la mémoire avec l'instruction FLD : celle-ci se comporte différemment suivant le flottant qu'on lui demande de charger. En effet, cette instruction peut charger depuis la mémoire un flottant simple précision, double précision ou double précision étendue. Le format du flottant qui doit être chargé est stocké directement dans l'instruction. Je m'explique : une instruction machine est stockée en mémoire sous la forme d'une suite de bits, et pour certaines instructions, des bits supplémentaires sont ajoutés. Dans notre cas, ces bits optionnels servent à indiquer à notre instruction le format du flottant qu'elle doit charger. La FPU x87 peut charger depuis la mémoire un nombre flottant 80 bits directement dans un registre. Pour les flottants 32 et 64 bits, la FPU va devoir effectuer une conversion de notre flottant simple ou double précision en un flottant 80 bits. Tous les calculs faits par notre FPU vont donner des résultats codés sur 80 bits, et ceux-ci restent codés sur 80 bits tant que ceux-ci sont stockés dans les registres de la FPU. Par contre, dès qu'il faut enregistrer un nombre flottant en mémoire RAM, les problèmes commencent. Si le flottant en question est stocké dans la mémoire sur 32 ou 64 bits, notre processeur doit convertir le contenu du registre dans le format du flottant en mémoire, histoire de conserver le bon format de base. Cette conversion est faite automatiquement par l'instruction d'écriture en mémoire utilisée. Par contre, si notre flottant est représenté en mémoire sur 80 bits, l'écriture en mémoire est directe : pas de conversion. Et ces conversions posent problème : elles ne respectent pas la norme IEEE 754 ! Comparons un calcul effectué sur un processeur gérant nativement les formats 64 et 32 bits et ce même calcul exécuté par la x87. Dans tous les cas, les flottants seront chargés dans les registres, le calcul s'effectuera et le résultat sera enregistré en mémoire RAM. Sur un processeur qui gére nativement les formats simple et double précision, ni le chargement, ni les calculs, ni l'enregistrement ne demanderont de faire des conversions vers des flottants 80 bits. Avec la x87, les flottants 32/64 bits sont convertis en un flottant x87 80 bits lors des échanges entre la pseudo-pile et la RAM. Les calculs sont effectués sur des flottants 80 bits uniquement, sans conversions. Lors de l'enregistrement d'un flottant x87 80 bits en mémoire, celui-ci est converti dans son format de base, au flottant 32 ou 64 bits le plus proche. On se retrouve donc avec un arrondi supplémentaire, en plus des arrondis liés aux calculs : c'est le phénomène du double rounding (qui signifie double-arrondi en français). Et rien n'implique que le résultat de ces deux conversions aurait donné le même résultat que le calcul effectué sur des flottants 64 bits ! [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées et qui avaient fait la une de la presse informatique étaient causées par ces arrondis supplémentaires. Bien sûr, sachez que ce bogue a pu être reproduit sur de nombreux autres langages et n'était certainement pas limité au PHP ou au Java : c'est le non-respect de la norme IEE754 par notre unité de calcul x87 qui était clairement en cause. De plus, si une série de calculs est faite sur des flottants stockés dans les registres, les résultats intermédiaires auront une précision supérieure à ce qui se serait passé avec des flottants simple ou double précision. Dans ces conditions, le résultat peut être différent de celui qu'on aurait obtenu en utilisant seulement des flottants 64 bits lors des calculs. Le pire, c'est qu'on n'a aucune solution à ce problème, pour les calculs faits avec l'extension x87. Autre problème, lié au précédent : rares sont les calculs effectués intégralement dans les registres, et on est parfois obligé de temporairement sauvegarder en mémoire le contenu d'un registre pour laisser le registre libre pour un autre nombre flottant. C'est le programmeur ou le compilateur qui gère quand effectuer ce genre de sauvegarde et sur quels registres. Chacune de ces sauvegardes va arrondir le flottant que l'on souhaite sauvegarder. Conséquence : suivant l'ordre de ces sauvegardes, le moment auquel elles ont lieu et les flottants qui sont choisis pour être sauvegardés, le résultat ne sera pas le même ! Avec le même programme, si vous décidez de sauvegarder un flottant et votre voisin un autre, ce ne sera pas le même flottant qui sera arrondi lors de son transfert en mémoire, et le résultat des calculs sur votre ordinateur sera différent des résultats obtenus sur l'ordinateur de votre voisin. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> smbpatj1dckbfhtlsz0sqgn5st3qe8v 763767 763766 2026-04-16T14:07:31Z Mewtow 31375 /* Le phénomène de double arrondi */ 763767 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> fpvc9vr2x5de3t90givpchu44jbxl6d 763768 763767 2026-04-16T14:08:40Z Mewtow 31375 /* Le phénomène de double arrondi */ 763768 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de '''double arrondi'' (''double rounding''). [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> j2g8pbldf8p2zagt3eoxqfn0r413pag 763769 763768 2026-04-16T14:10:42Z Mewtow 31375 763769 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. Sur les tout premiers processeurs x86, le support du x87 n'était pas implémenté dans le processeur. Il était délégué à des coprocesseurs, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Par la suite, les processeurs x86 ont intégré une FPU qui exécutait les instructions x87 nativement. Les coprocesseurs sont devenus inutiles. Nous verrons en détail ces coprocesseurs dans le prochain chapitre. Pour le moment, nous allons juste parler du jeu d'instruction x87. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de '''double arrondi'' (''double rounding''). [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Le modèle mémoire : alignement et boutisme | prevText=Le modèle mémoire : alignement et boutisme | next=Les composants d'un processeur | nextText=Les composants d'un processeur }} </noinclude> r38q5wxg53ugwa3tc82175yt9txagde 763774 763769 2026-04-16T14:12:39Z Mewtow 31375 /* Le phénomène de double arrondi */ 763774 wikitext text/x-wiki Dans ce chapitre, nous allons étudier une extension de l'architecture x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps, tout en gardant la compatibilité avec les versions plus anciennes. Les ajouts en question sont appelés des '''extensions x86'''. Dans ce chapitre, nous allons étudier l'extension qui a ajouté le support des nombres flottants au x86. Elle a ajouté des instructions, mais aussi des registres, spécialisés dans les nombres flottants. L'ensemble s'appelle l''''extension x87'''. L'extension x87 est encore utilisée par défaut sur les PC 32 bits. Par contre, avec le jeu d'instructions x86-64 bits, c'est une autre extension qui est utilisée pour les calculs flottants, l'extension SSE. Sur les tout premiers processeurs x86, le support du x87 n'était pas implémenté dans le processeur. Il était délégué à des coprocesseurs, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Par la suite, les processeurs x86 ont intégré une FPU qui exécutait les instructions x87 nativement. Les coprocesseurs sont devenus inutiles. Nous verrons en détail ces coprocesseurs dans le prochain chapitre. Pour le moment, nous allons juste parler du jeu d'instruction x87. ==Les registres x87== L'extension x87 fournit, en plus des instructions, plusieurs registres : * 8 registres pour les opérandes flottantes ; * 3 registres d'état pour configurer les exceptions, les arrondis, etc ; * 1 registre utilisé pour gérer les exceptions flottantes, auquel seul le processeur a accès. ===Une organisation en pseudo-pile=== Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 et sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Le registre d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Il fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. Pour simplifier, le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Et il indique aussi si le registre contient des valeurs spéciales : infini : zéro, dénormal, etc. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ==Les instructions flottantes x87== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. Elles peuvent se classer en deux types : des instructions de calcul, des instructions d'accès mémoire. ===Les instructions de calcul x87=== On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT, le calcul de la valeur absolue (FABS) ou encore un changement de signe (FCHS). Fait étonnant, elle gérait des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. En voici la liste : * l'instruction FCOS pour le cosinus ; * l'instruction FSIN pour le sinus ; * l'instruction FPTAN pour la tangente ; * l'instruction FPATAN pour l'arc tangente ; * des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. ===Les instructions mémoire x87=== Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ==Le phénomène de double arrondi== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. La taille du flottant était précisée dans le mode d'adressage, avec quelques bits de l'instruction réservés pour. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Les instructions x87 font des calculs sur des flottants 80 bits uniquement, elles n'ont pas de support pour les flottants 32 ou 64 bits. La conséquence est que les lectures/écritures font des conversions de flottants. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est automatiquement étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Un problème est que faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. Pire que ça, le résultat du calcul dépend de l'ordre dans lequel on fait les instructions. Les arrondis seront différents selon que l'on fait une écriture à tel moment plutôt qu'un autre. Il s'agit du phénomène dit de '''double arrondi'' (''double rounding''). [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les sections critiques et le modèle mémoire | prevText=Les sections critiques et le modèle mémoire | next=Les coprocesseurs : FPU et IO | nextText=Les coprocesseurs : FPU et IO }} </noinclude> 46xc6jj8sn7zvefq3fmotcrgxge5822 Photographie/Personnalités/A/James Aurig 0 73987 763839 763743 2026-04-17T00:26:19Z Speravir 46823 /* Galerie de photographies */ File renamed: [[:File:BfAuK IV 101 Wohnhaus Humboldstrasse 3 Haus Potsdam.jpg]] → [[:File:BfAuK IV 101 Wohnhaus Humboldtstrasse 3 Haus Potsdam.jpg]], [[c:COM:FR#FR3|criterion 3]]: error in filename 763839 wikitext text/x-wiki {{Ph s Personnalités}} [[Fichier:James Aurig - Selbstporträt 1900.jpg|thumb|240px|autoportrait, 1900]] == Biographie == '''James Aurig''' était un photographe allemand né le 28 août 1857 à Guben et décédé le 19 décembre 1935 à Blasewitz. == Publications == == Galerie de photographies == <gallery widths="240px" heights="240px"> 1910 De Backer 1.jpeg Arthur Willibald Koenigsheim.jpg Aurig James Hospiz Stadtverein Brunnen Dresden.jpg Aurig James Hospiz Stadtverein Dresden.jpg Aurig James Kaiserpalast Dresden.jpg Aurig James Kaiserpalast Ostseite Dresden.jpg Aurig James Landhaus Dresden.jpg BfAuK IV 101 Wohnhaus Humboldtstrasse 3 Haus Potsdam.jpg BfAuK IV 102 Noacksches Wohnhaus Potsdam.jpg BfAuK V 55 Kraatzsches Haus Potsdam.jpg BfAuK V 56 Zeisingsches Haus Potsdam.jpg BfAuK V 74 Wohnhaus Wilhelmplatz 5 Potsdam.jpg BfAuK VI 119 Wohnhaus Wilhelmplatz 8 Potsdam.jpg James Aurig - Arthur Willibald Königsheim.jpg James Aurig - Karl August Lingner.jpg James Aurig - Karl Emil Scherz.jpg James Aurig - Prinz Johann Georg von Sachsen in seinem Arbeitszimmer.jpg James Aurig - Selbstporträt 1900.jpg James Aurig - Sächsisches Königshaus.jpg Ludoviko Zamenhof - Aurig.jpg S.M. Friedrich August III. von Sachsen 27.jpg S.M. Friedrich August III. von Sachsen 32.jpg S.M. Friedrich August III. von Sachsen 9.jpg Zum Hausvoigt 1892.png </gallery> == Bibliographie == {{Ph Personnalités}} {{DEFAULTSORT:Aurig, James}} [[Catégorie:Personnalités de la photographie]] saa1ynp7q1tj2c9lf28p5m3fz33otqy Mathc initiation/Fichiers h : c44a4 0 76524 763831 763720 2026-04-16T21:57:02Z Xhungab 23827 763831 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF. * [https://openstax.org/details/books/calculus-volume-1 Calculus 1], * [https://openstax.org/details/books/calculus-volume-2 Calculus 2], * [https://openstax.org/details/books/calculus-volume-3 Calculus 3]. : {{Partie{{{type|}}}|[[Mathc initiation/a08| Analyse I ; Les fonctions]]}} : {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c60a3| Analyse I : Les courbes paramétriques]]}} : {{Partie{{{type|}}}|[[Mathc initiation/c58a3| Analyse I : Les fonctions vectorielles]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a334| Analyse I : Les suites et les séries]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a16| Analyse II : Fonction de plusieurs variables]]}} {{Partie{{{type|}}}|[[Mathc initiation/005v| Analyse II : Dérivées partielles. Méthode de Newton (en xy). Applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/005u| Analyse II : Dérivée d'une fonction implicite]]}} {{Partie{{{type|}}}|[[Mathc initiation/005t| Analyse II : Dérivées des fonctions composées]]}} {{Partie{{{type|}}}|[[Mathc initiation/005w| Analyse II : Calculer le gradient et quelques applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/0062| Analyse II : Calculer la divergence]]}} {{Partie{{{type|}}}|[[Mathc initiation/0068| Analyse II : Calculer le rotationnel]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a09| Analyse III : Intégrale doubles et applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/0044| Analyse III : Intégrale triples et applications]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005j| Analyse III : Petite introduction sur les intégrales curvilignes]]}} {{Partie{{{type|}}}|[[Mathc initiation/0045| Analyse III : Intégrale curvilignes et applications.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005k| Analyse III : Petite introduction sur les champs de vecteurs]]}} {{Partie{{{type|}}}|[[Mathc initiation/005l| Analyse III : Intégrale curvilignes dans un champ de vecteurs.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005h| Analyse III : L'intégrale de surface]]}} {{Partie{{{type|}}}|[[Mathc initiation/005s| Analyse III : L'intégrale de flux de surface]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/004w| Analyse III : Théorème de Gauss (Théorème de la divergence)]]}} {{Partie{{{type|}}}|[[Mathc initiation/005i| Analyse III : Théorème de Green, de Stoke]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse III : Les équations différentielles]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les series de Fourier]]}} : . : {{Partie{{{type|}}}|fond={{{fond|}}}|prefixTable=I - |prefix1=Troisième Partie : | '''Bibliothèques'''}} : Pour éviter de devoir télécharger les bibliothèques à chaque sections, je vous propose ici de télécharger les fichiers le plus souvent utilisés. {{Partie{{{type|}}}|[[Mathc initiation/a406| La bibliothèque d'analyse I, II, III :]]}} : {{AutoCat}} 1p6s9efajuroj6995kbsfm1210jsnpd 763862 763831 2026-04-17T11:45:13Z Xhungab 23827 763862 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : Je vous propose comme cours de référence les trois livres de '''openstax''' en accès libre. Vous pouvez les lire en ligne ou télécharger les PDF. * [https://openstax.org/details/books/calculus-volume-1 Calculus 1], * [https://openstax.org/details/books/calculus-volume-2 Calculus 2], * [https://openstax.org/details/books/calculus-volume-3 Calculus 3]. : {{Partie{{{type|}}}|[[Mathc initiation/a08| Analyse I ; Les fonctions]]}} : {{Partie{{{type|}}}|[[Mathc initiation/Fichiers h : c60a3| Analyse I : Les courbes paramétriques]]}} : {{Partie{{{type|}}}|[[Mathc initiation/c58a3| Analyse I : Les fonctions vectorielles]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a334| Analyse I : Les suites et les séries]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a16| Analyse II : Fonction de plusieurs variables]]}} {{Partie{{{type|}}}|[[Mathc initiation/005v| Analyse II : Dérivées partielles. Méthode de Newton (en xy). Applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/005u| Analyse II : Dérivée d'une fonction implicite]]}} {{Partie{{{type|}}}|[[Mathc initiation/005t| Analyse II : Dérivées des fonctions composées]]}} {{Partie{{{type|}}}|[[Mathc initiation/005w| Analyse II : Calculer le gradient et quelques applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/0062| Analyse II : Calculer la divergence]]}} {{Partie{{{type|}}}|[[Mathc initiation/0068| Analyse II : Calculer le rotationnel]]}} {{Partie{{{type|}}}|[[Mathc initiation/0069| Analyse II : Calcul de R_s_x_R_t]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a09| Analyse III : Intégrale doubles et applications]]}} {{Partie{{{type|}}}|[[Mathc initiation/0044| Analyse III : Intégrale triples et applications]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005j| Analyse III : Petite introduction sur les intégrales curvilignes]]}} {{Partie{{{type|}}}|[[Mathc initiation/0045| Analyse III : Intégrale curvilignes et applications.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005k| Analyse III : Petite introduction sur les champs de vecteurs]]}} {{Partie{{{type|}}}|[[Mathc initiation/005l| Analyse III : Intégrale curvilignes dans un champ de vecteurs.]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/005h| Analyse III : L'intégrale de surface]]}} {{Partie{{{type|}}}|[[Mathc initiation/005s| Analyse III : L'intégrale de flux de surface]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/004w| Analyse III : Théorème de Gauss (Théorème de la divergence)]]}} {{Partie{{{type|}}}|[[Mathc initiation/005i| Analyse III : Théorème de Green, de Stoke]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a10| Analyse III : Les équations différentielles]]}} : . : {{Partie{{{type|}}}|[[Mathc initiation/a512| Analyse IV : Se familiariser avec la Transformée de Laplace]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a584| Analyse IV : Se familiariser avec la transformée en Z]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a592| Analyse IV : Se familiariser avec la transformée de Fourier discrète]]}} : {{Partie{{{type|}}}|[[Mathc initiation/a594| Analyse IV : Se familiariser avec les series de Fourier]]}} : . : {{Partie{{{type|}}}|fond={{{fond|}}}|prefixTable=I - |prefix1=Troisième Partie : | '''Bibliothèques'''}} : Pour éviter de devoir télécharger les bibliothèques à chaque sections, je vous propose ici de télécharger les fichiers le plus souvent utilisés. {{Partie{{{type|}}}|[[Mathc initiation/a406| La bibliothèque d'analyse I, II, III :]]}} : {{AutoCat}} i938zs2rw15w77vrm7o2ea2cv7svuel Utilisateur:Matthius/Conseils-Chretiens 2 77816 763852 749600 2026-04-17T06:30:08Z Ziv 119944 ([[c:GR|GR]]) [[File:BrugghenDoubtingThomas.jpeg]] → [[File:De ongelovige Thomas Rijksmuseum SK-A-3908.jpeg]] → File replacement: Updating from an old version to a newer version with better quality ([[c:c:GR]]) 763852 wikitext text/x-wiki {{Problème de neutralité}} {{Sources}} = Conseils pour les Chrétiens = <center> Matthieu Giroux Coaching Scientifique par des Solutions Éditions LIBERLOG Éditeur n° 978-2-9531251 ISBN E-Book 9791092732795 ISBN Livre 9791092732788 [http://www.archive.org/details/ConseilsChretiens www.archive.org/details/ConseilsChretiens] Droits d'auteur 2021 Licence Creative Common by SA </center> == Du même auteur == * Pourquoi un Dieu ? * Conseils pour les Chrétiens * L’Univers est Vivant ! * L’Univers pour les Enfants * La Bataille (Sortie en 2022) * Morale Économique des Métiers * L'Économie pour les Enfants * L'Économie pour les Petits * Expliquer sa Religion Chrétienne * Devenir un Génie * Favoriser la Créativité * L'Économie est Physique. * La généalogie c'est gratuit, avec les logiciels libres * France – Fonctionnement * Les Deux France * La Restauration * LAZARUS FREE PASCAL - DR * LAZARUS Développement Rapide - 3e édition * L'Astucieux GNU LINUX (Wikibook Ubuntu) * Comment écrire des histoires * Poèmes et Sketchs – De 2003 à 2008 * Nos Nouvelles Nos Vies * Nouvelles Courtes Visibles sur livrels.fr, en wikilivres, les sources sur archive.org/details/Scribels. À[http://liberlog.fr/ liberlog.fr],[http://www.comment-ecrire.fr/ comment-][http://www.comment-ecrire.fr/ ecrire.fr],[http://www.favorisercreativite.fr/ favorisercreativite.fr],[http://www.simplifierlecole.com/ simplifierlecole][http://www.simplifierlecole.com/ .com],[http://www.universvivant.fr/ universvivant.fr],[http://www.conseilschretiens.fr/ conseilschretiens.fr],[http://www.quiestdieu.net/ pourquoidieu.fr],[http://www.etrechrist.com/ etrechrist.com],[http://www.lazarus-components.org/ <span style="font-size:12pt;font-family:'Liberation Serif'">lazarus</span>][http://www.lazarus-components.org/ <span style="font-size:12pt;font-family:'Liberation Serif'">-</span>][http://www.lazarus-components.org/ <span style="font-size:12pt;font-family:'Liberation Serif'">components.org</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.devenirgenial.fr/ <span style="font-size:12pt;font-family:'Liberation Serif'">devenirgenial.fr</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.ecoreel.fr/ <span style="font-size:12pt;font-family:'Liberation Serif'">ecoreel.fr</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://courseconomie.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">course</span>][http://courseconomie.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">conomie.com</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.economiepetits.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">ecopetits.fr</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.france-analyse.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">france-analyse.com</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.ethiquetravail.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">ethiquetravail.com</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.programmationinfos.com/ <span style="font-size:12pt;font-family:'Liberation Serif'">informalibre.com</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">,</span>[http://www.histoire.ovh/ <span style="font-size:12pt;font-family:'Liberation Serif'">histoire.ovh</span>]<span style="font-size:12pt;font-family:'Liberation Serif'">.</span> == Du même éditeur == * Lyndon Larouche * Mon Père m'a Dit Elliott Roosevelt * Henry Charles Carey * Emer De Vattel == À Lire == Je vous conseille les liseuses à encre électronique tactiles non affiliées à un site web de vente de livrels. Même si elles sont plus chères, elles vous permettront de lire l'ensemble des auteurs présents dans mes livres. Les liseuses tactiles ou scribeuses permettent d’écrire pour créer ce qu’on veut pour plus tard. Quiconque peut devenir scribeur donc peut agir mondialement. Un écrivain public utilise les scribeuses pour écrire mondialement. Les images viennent de wikimedia commons. Bonnes lectures et écritures ! = Préface = Ce livre est une réponse aux conférences du pasteur Miki HARDI, ainsi qu’aux émissions radios de RCF Alpha et Radio Notre Dame, aux émissions des vlogs Dieu Sauve et Frère Paul Adrien. = Des Sacrements = Si on a franchi une étape on peut la sanctifier avec un sacrement. Les 7 sacrements de l’Église ne sont pas les sacrements que je présente ici. J’utilise le mot sacrement ici pour une étape à confirmer par l’Église. Le sacrement des malades n’est pas mentionné ici. C’est un des sept sacrements de l’Église. Seulement un patient peut aussi se tourner vers les autres sacrements. == Le Baptême == Lundi 27 septembre 2021 [[Image:Pierre Puget - Le baptême de Clovis.jpg|alt=|droite|vignette|upright|<center>Baptême de Clovis</center>]] Jésus s’est fait baptiser pour aller chercher la vérité. Le baptême consiste donc à aller chercher la vérité, que ce soit par les parents ou par soi-même. On cherche sa vérité et la vérité avec les autres, pour développer les autres et se développer soi. On fournit les connaissances qu’il s’agit de lire aux autres. Il y a par exemple Maître Eckhart qui donne un chemin ou Henry Charles Carey qui contredit la décroissance. On crée alors des cartes de visite pour faire plus simple. === Mes Notes === Écrire des poèmes sur sa vérité. == La Confirmation == Lundi 27 septembre 2021 Devenir prophète est l’aboutissement d’un chrétien. Ainsi la confirmation demande au chrétien d’essayer de devenir prophète, c’est à dire d’envisager un avenir sain. Un prophète parle ou écrit mondialement sur la société ou les autres. Il combat le libéralisme qui vise à individualiser l’individu quand on est en récession. Il vous permet d’être aidé par votre ou la vérité. Le prophète se place toujours derrière Dieu. [[Image:Elisabeth Keyser - A Confirmand in Normandy - NM 7478 - Nationalmuseum.jpg|alt=|droite|vignette|upright|<center>Une confirmande – Élisabeth Keyser</center>]] Pour discerner un prophète il faut être habitué à voir le vrai et le faux dans une parole de vérité. Le prophète dit vrai sur tout car il est aidé par l’esprit. Le prophète est quelqu’un qui a porté sa croix en disant la vérité. Le prophète aime. Une prophétie met du temps à être réalisée. Le prophète ne connaît pas exactement le plan de Dieu. Il le devine parce qu’il est dans la vérité, vérité qui peut surprendre en bien. Ainsi, le seul prophète qui restera à l’apocalypse selon Saint Jean sera Jésus. Un prophète intervient quand il y a du chaos. Si le chaos est mondial, alors le prophète est aidé par Jésus. Le prophète est celui qui construit le monde idéal avec les autres en les convaincant. Il a les solutions qui permettent de construire ce monde. Un prophète fait donc de la politique. On écrit des poèmes puis on écrit sur soi pour relire ces écrits plus tard. On discute avec les autres pour trouver la vérité, puis on donne la vérité qu’on pense juste. On essaye d’anticiper l’avenir en trouvant des relations avec la Bible ou avec des livres de développement personnel par la société. Puis on apprend la communication, communication qui sera une de nos intelligences. On milite pour un monde à construire. On écoute ceux qui sont en accord avec la Bible. Alors notre vie avec les autres et notre chemin de vérité fera de nous peut-être un prophète. Pour changer, il faut être ouvert à la correction. Il faut donc avoir envie d’évoluer. Le prophète n’est pas un beau parleur mais quelqu’un qui a écouté. === Mes Notes === Essayez d’envisager l’avenir ou écrivez un poème. == Le Mariage == Lundi 27 septembre 2021 [[Image:Mariage de Louis de France, duc de Bourgogne.jpg|alt=|droite|vignette|upright|<center>Mariage de Louis de France, duc de Bourgogne</center>]] Deux époux mariés ne doivent faire qu’un. L’amour ne suffit donc pas pour se marier. Serons-nous toujours chacun de notre côté après le mariage ? Ça n’est pas le mariage qui unit. Les deux fiancés doivent être unis avant de se marier. Les émotions doivent rester de côté pour prendre des décisions. Ainsi on écrit des poèmes pour comprendre ses émotions. Il faut savoir dire ce qui ne nous convient pas ou donner la bonne voie à suivre. C’est ce que font les amis. Il s’agit donc de réfléchir tout haut avec son conjoint. On doit savoir résister au monde du diable ensemble. Si sa famille va dans le mauvais chemin, il s’agit de résister. Il ne faut savoir protéger sa vie des mauvais chemins en évitant les personnes malsaines. Si on les aime, il s’agit de les écouter mais pas de les suivre. Si son conjoint à subi un échec, pas trop grave, c’est normalement plus facile de lui parler car il est plus à l’écoute. Sinon, il s’agit d’envisager que sans bon chemin, il y aura un échec. Alors, il s’agit de deviner cela et de devenir prophète sur son conjoint. Ainsi des conflits seront évités. === Mes Notes === Écrire sur ceux que vous aimez. == Devenir Prêtre == Lundi 27 septembre 2021 [[Image:Prêtre célébrant une messe.jpg|alt=|droite|vignette|upright|<center>Prêtre célébrant une messe.</center>]] Beaucoup de chrétiens et d’athées voient le prêtre comme quelqu’un à part, quelqu’un d’éthique et de moral, plus que soi-même. Le prêtre est très souvent un juste qui a décidé de se donner à Jésus. Il exprime donc le message de Dieu mais reste aussi juste que vous et moi. L’église décidera si vous serez prêtre. Suivre la théologie permettra de parcourir le chemin de vérité pour devenir prêtre. === Mes Notes === Créer un atelier pour la bonne nouvelle qu’on veut donner. == Devenir Moine ou Religieuse == Lundi 27 septembre 2021 [[Image:Eugénie Guillou.jpg|alt=|droite|vignette|upright|<center>Religieuse</center>]] Il ne s’agit pas de se forcer à devenir moine ou religieuse. Le moine ou la religieuse a une vie calme mais remplie, une vie en dehors des activités de la ville. Mais il ou elle médite sur tout ce qui l’environne en priant. Alors le calme l’atteint. Il ou elle voit Dieu en s’oubliant. Certaines fois des personnes se réfugient dans les monastères parce qu’ils en ont marre du monde libéral où le plus fort a raison. Ces personnes ne pourraient normalement pas devenir moine ou religieuses. Mais elles sont acceptées parce qu’on peut convertir petit à petit. Les moines et religieuses n’aiment pas le monde libéral. Ils ou elles savent ce qu’est vivre en société. Il ou elles retrouvent une société fraternelle en devenant moine ou religieuse. === Mes Notes === Chercher à trouver le calme. = La Religion = Il s'agit d'apporter la paix et le développement. Voyons les contradictions et allons chercher vérité sur des religions mises à mal. == Le Péché Originel == Lundi 1er Novembre 2021 [[Image:Hubble ultra deep field high rez edit1.jpg|alt=|droite|vignette|upright|<center>Notre univers est organisé</center>]] Le péché originel c’est en réalité le fait que l’humain est imparfait. Dieu a été engendré par l’univers ou un autre Dieu. Il a ensuite généré des anges, dont le diable, diable qui représente le péché originel. Le diable qui séduit l’humain, c’est sur Terre, pas dans le monde de Dieu. Les humains ont chassé le diable du monde de Dieu, comme le dit Jésus dans les évangiles. Dieu a en réalité créé notre univers quand il a vu que le diable gênait les autres anges, avec la quête de pouvoir du diable. Dieu s’est en réalité rendu compte que les anges pouvaient être imparfaits. Donc il a attendu que notre univers crée des êtres intelligents pour s’occuper du diable, sachant qu’il fait défiler le temps comme il veut. En effet, c’est une vitesse infinie qui a créé la première vie. RegarderPourquoi un Dieu ? À ce propos. === Mes Notes === Écrire à propos du premier univers. == À propos des Juifs et Jésus == jeudi 23 septembre 2021 [[Image:Jesus with the cross in Duomo (San Gimignano).jpg|alt=|droite|vignette|upright|<center>Des juifs ont aidé Christ et sont devenus chrétiens.</center>]] Longtemps les chrétiens ont réprimé les juifs parce qu’ils auraient tué un Dieu. Tout d’abord, Jésus était juif. Donc Jésus a été dénoncé par les siens. Ensuite, les chrétiens ont convaincu les juifs. Il est donc possible que le juif qui a dénoncé Jésus à Pilate, pour savoir si Jésus résistait à la mort pour sa crucifixion, ait ensuite été convaincu de devenir chrétien. Aussi, Dieu a voulu que Jésus soit dénoncé par les siens pour ne pas mettre les peuples les uns contre les autres. La rançon sert à dire que l’on perdra tout ce que l’on a possédé sur Terre signifie que les financiers n’iront que rarement dans le monde de Dieu. Le monde de Dieu se méfie de ceux qui ne pensent qu’à eux-mêmes, comme par exemple à dire qu’on serait le peuple élu, que ce soit pour les chrétiens ou pour les juifs. Dieu veut accueillir les justes. Il préfère un chrétien dans l’âme plutôt qu’un chrétien dans les normes. === Mes Notes === Discuter avec les autres religions. == À propos de l’Islam == Samedi 25 septembre 2021 [[Image:Islamic marriage.jpg|alt=|droite|vignette|upright|<center>Mariage islamique</center>]] Outre les fanatiques de la religion qui veulent entrer en contradictions avec vous, on rencontre plus souvent des personnes douces qu’on oublie. Les fidèles de l’Islam qui vivent dans notre pays veulent s’intégrer, tout comme les chrétiens non prioritaires dans d’autres pays le font. La religion sert à apporter et porter la paix. Donc ne nous écharpons pas entre religions. Au contraire, découvrir une nouvelle religion enrichit historiquement donc théologiquement. Que c’est beau de discuter entre religions. === Mes Notes === Discuter avec les autres religions. = La Plénitude = == Se Mettre en Mouvement == Lundi 6 décembre 2021 La vie c’est le mouvement. Par exemple, si nous ne faisons rien, nous oublions. La vie ne peut rester telle qu’elle était il y a peu. Donc être dans le mouvement de la vie est primordial. C’est la créativité qui permet d’être dans le mouvement de la vie. Oser imaginer son avenir permet d’anticiper l’avenir. Écrire permet de se mettre dans le mouvement de la vie. Méditer permet d’anticiper de futures situations, pour peu qu’on ne regarde pas des images rapides. Faire bouger son corps permet de sentir la vie. On cherche pour trouver des solutions et poser les vrais problèmes. Si on n’a pas de solutions, on se dit que Dieu en a. Donc c’est à nous de trouver les solutions. Par exemple, le CO2 utilise 0,04 % de l’atmosphère. Les plantes grandissent plus vite depuis qu’il augmente. Donc il n’y a pas assez de CO2, contrairement à ce qui est dit. En effet, il y avait 1,7 % de CO2 pendant le Crétacé, au moment où les plantes poussaient vite. Suivre le mouvement de la vie s’est s’accaparer sa vie, sentir qu’on vit, savoir réagir correctement à une mauvaise situation, en étant préparé, en se demandant pourquoi on vit, pourquoi on est là à cet instant. Un ami nous aide à voir cela. Vouloir devenir parfait permet de chercher la perfection de Dieu. Nous ne serons jamais parfait. C’est pourquoi poser des questions pour sentir la perfection permet de se sublimer. Vouloir devenir parfait permet de comprendre le mouvement de la vie. On devient un génie en comprenant le mouvement de la vie et de la perfection. Se parfaire c’est savoir agir avec les solutions au moment où le problème vient. Si on a su réagir correctement à une mauvaise situation, alors ça veut dire qu’on comprend la nature, qu’on est scientifique donc. Chacun a sa façon de procéder. Lire sur ses intelligences multiples permet de voir comment on pense. === Mes Notes === Écrire sur les autres et leur nature bénéfique. == La Distraction == Jeudi 21 octobre 2021 On aime se distraire. Seulement beaucoup de distractions sont malfaisantes en temps de récession. L’objectif de la finance est de faire de nous des animaux par les émotions pour que nous nous tapions dessus entre nous. Si l’état dirige réellement avec sa monnaie publique, alors on trouve des émissions créatives. Seulement, les émissions animales auront fait de nous un animal. Les documentaires peuvent avoir des informations vraies en temps de récession. Les informations sont vraies quand votre pays se développe. Il y a des émissions de divertissement qui mettent en valeur la créativité humaine. Seulement on passe beaucoup de temps à attendre ces émissions en temps de récession. === Mes Notes === Écrire sur ce que vous regardez. == La Peur == Jeudi 21 octobre 2021 [[Image:The Man Made Mad with Fear by Gustave Courbet.jpg|alt=|droite|vignette|upright|<center>Gustave Courbet - L'homme ayant peur</center>]] La peur empêche de voir le plan de Dieu pour nous. Il faut donc la comprendre. On a tous peur de quelque chose. Mais c’est anormal d’avoir peur de la première chose venue. La distraction par les émotions amène la peur de tout. Avoir peur de Dieu est une bonne peur. La crainte de Dieu est normale. Il nous a créé. Concrétiser sa peur et l’écrire permet de la voir plus tard comme quelque chose de commun. Alors on identifie le problème qui crée la peur. Pour résoudre une peur qui revient trop souvent, il s’agit de se mettre en état de réflexion afin que l’esprit nous tourne pas en rond sur la peur. === Mes Notes === Écrire sur sa peur. == Méditer == Samedi 25 septembre 2021 La méditation est importante. On relie son esprit pour qu’il soit un. La méditation permet d’être serein pour pouvoir improviser quand un païen s’interroge. Alors notre dialogue est bref et ciblé. C’est normal. Il s’agit de ne pas parler dans le vide. Vous serez étonné de savoir comment vous avez convaincu. Les gens s’intéressent aux autres. C’est votre histoire qui intéresse. Alors prier en méditant permet de se rapprocher des autres. === Mes Notes === Après avoir fait une activité, méditer pour recréer les liens dans l’esprit. Lire Devenir un Génie. == Prier == Lundi 27 septembre 2021 [[Image:Bernadette Soubirous en 1861 photo Bernadou 4.jpg|alt=|droite|vignette|upright|<center>Bernadette SOUBIROUS en train de prier.</center>]] Prier c’est penser aux autres. Le faire un tout petit peu est déjà grand. On se force au début puis on pense un tout petit peu aux autres. Prier pour agir c’est encore mieux. Alors on pense aux autres dans les moments de repos, quand on médite. Alors on agit avec les autres. Puis un jour on naît de nouveau. On trouve des solutions plus facilement alors. Après on prie sans s’en rendre compte. === Mes Notes === Prier. == Avec les Émotions == dimanche 26 septembre 2021 [[Image:Aimée Thibault - The King of Rome (Napoleon II), as a child, writing to his father (Napoleon I).jpg|alt=|droite|vignette|upright|<center>Aimée Thibault - Napoléon II en train d'écrire</center>]] Quand on est sous le choc on est prêt à tout enregistrer pour y repenser plein de fois. Ainsi dire « Dieu t’aime » à quiconque n’importe quand permet à celui qui écoute de se sentir aimé. Il est probable alors qu’il s’en souviendra pour la vie. On vit dans un monde d’émotions. Les émotions peuvent apporter la guerre. Seulement, on vit longtemps donc on ne veut pas partir en guerre. Alors l’amour prend toute sa place, surtout si on n’est pas habitué à recevoir de l’amour. En effet, les émotions ne sont rien face à l’amour du prochain. Évidemment, on commence par dire qu’on aime ses proches. Sinon il y aura des jaloux. N’oubliez pas qu’un couple ne doit faire qu’un pour ne pas choisir avec les émotions. Si vous voyez le moindre frein à ne pas choisir votre futur conjoint, réfléchissez pour savoir si vous ne ferez qu’un avec lui. Écrire des poèmes pour comprendre ses émotions permet de se voir comme un autre plus tard. Alors on peut se dire si on était fou ou simplement dans la bonne voie en pensant à ce qui nous mène. === Mes Notes === Écrire un poème. == La Chair ou l’Esprit == Jeudi 21 octobre 2021 Tant qu’on est dans la chair, on a peur d’être libre. Au début de sa liberté, on a très peur. Mais on trouve ensuite des nouveaux repères. On est alors dans l’esprit. On peut voir la liberté comme quelque chose de grand, que ce soit avant ou après que son cœur soit dans l’esprit. La liberté est pourtant apeurante. Il ne s’agit pas de s’obliger à être libre. On ne le serait pas. Il s’agit d’aimer et d’aller vers l’amour, celui qui libère. On va contre la tentation de se laisser faire. [[Image:Drago - Piero di Cosimo - Andromeda Perseo.jpg|alt=|droite|vignette|upright|<center>Piero di Cosimo – Les divertissements nous font croire n’importe quoi.</center>]] Beaucoup s’arrêtent à la peur d’être libre, c’est à dire de se voir retirer les chaînes du monde du diable. Alors on se complaît dans la chair, se disant que ceux qui sont libres n’y ont pas droit, ce qui est faux. On a surtout comme Dieu la chair ou l’argent. Au contraire, quand on est libre, le plaisir de la chair est amplifié. Seulement on se rend compte qu’il est répétitif. Alors le plaisir de la chair vient quand on veut simuler quelque chose de nouveau. Nous ne sommes pas tentés par quelque chose de plus fort que nous. La loi permet de suivre le chemin de vérité. Les dix commandements sont surtout faits pour ceux qui cherchent à devenir libres. Ensuite il y a « aime ton prochain comme toi même. ». Quand on est dans l’esprit, nos sens se développent par la méditation, le dialogue ou l’écriture. C’est pour cela que la chair est encore plus présente. Seulement, notre esprit agit plus donc on préfère être dans l’esprit. Nos sens sont mieux perçus quand nous sommes libres et par notre âge. === Mes Notes === Écrire sur la liberté. == J’ai une Addiction == Jeudi 21 octobre 2021 [[Image:MacauOpiumSmoking20c.jpg|alt=|droite|vignette|upright|<center>L'Opium est addictif.</center>]] Ceux qui n’ont jamais eu d’addiction grave sont capables de vivre sans avoir peur d’être addict. Ils vivent sereinement. Par contre, quand on a une addiction grave, la chair et la vie sont vus différemment. Ce manque ne sera mineur que quand nous deviendrons justes, à notre mort. C’est une fois libre que son cœur dit qu’on ne veut plus de l’addiction. Là le cœur a été formé. Les autres addicts sont reconnaissants de vos paroles. Seulement il faut avant que le cœur parle former le cœur et s’empêcher d’aller vers l’addiction. Encore une fois, on se crée une loi et on la respecte. Si cette loi est brisée on la promulgue de nouveau. === Mes Notes === Écrire sur ce que vous aimez le plus faire. = L’Ego = Notre ego est imparfait dès la naissance. Au début, notre ego veut se satisfaire de lui-même. Seulement, en découvrant les autres on se découvre soi. Donc on remarque que notre ego empêche de se voir. Si on veut défier sa peur, on va de plus en plus vers les autres pour évoluer vers un être social. == Je me sens Égoïste == vendredi 12 novembre 2021 [[Image:Wilhelm Marstrand, Fra et romersk osteria, piberygende jægere og italierinder, udateret, 0216NMK, Nivaagaards Malerisamling.jpg|alt=|droite|vignette|upright|<center>On peut prendre des décisions en petit comité. Wilhelm Marstrand.</center>]] L’égoïsme permet de déceler l’intérêt général. En effet l’individualisme ne permet pas de créer de nouveaux droits alors que l’intérêt général le permet. Donc quelqu’un qui défend l’intérêt général veut de nouveaux droits pour lui-même. C’est une vision à long terme. Si son intérêt général se réalise, alors on aura tous de nouveaux droits. Celui qui défend l’intérêt général saura en bénéficier en premier. L’intérêt général permet de créer tous les jours en tant que salarié, d’être rémunéré pour des médias créés. Seulement, ça n’est pas le cas actuellement. Donc il s’agit de créer une monnaie publique qui permette de protéger nos industries. Comme cela, cet intérêt général permettra de créer tous les jours. === Mes Notes === Écrire sur l’intérêt général et les industries. == On me Dit que je Suis Moral == Vendredi 26 novembre 2021 Être moral ça peut être bien se on prend en compte la morale de l’autre. Il ne faut pas se sentir à part mais savoir que tout le monde peut être moral. Nous avons tous une place dans la société, même ceux qui peuvent être immoraux. Peut-être que votre métier est moral. Seulement, la société est-elle morale ? Beaucoup voudraient que la morale soit active dans la société. Seulement, la morale nécessite une vision à long terme dans la société. Cette vision à long terme vous permet d’organiser votre entourage par l’éthique, une autre façon d’améliorer la morale. === Mes Notes === Écrire sur la société que vous voulez organiser à long terme. == L’Espérance == mercredi 6 octobre 2021 [[Image:LDes Coudres-Magdalene.jpg|alt=|droite|vignette|upright|<center>Sainte Marie Madeleine</center>]] L’espérance c’est se dire qu’il y aura au moins une bonne nouvelle dans la journée pour pouvoir faire le bilan de la journée le soir. L’espérance c’est aussi participer à la construction du monde ou de l’église. L’humain est fait pour construire et exécuter des tâches de construction. Le diable veut que l’on soit négatif. Il ajoute mondialement de fausses informations sur une déchéance humaine. Il s’agit de trouver ceux qui contredisent ces mensonges, de trouver une raison scientifique et historique à la vérité, celle qui nous donne espérance. Alors on communique notre espérance avec cette vérité pour défaire le plan du diable. Alors on est capable d’accepter la souffrance de son corps. === Mes Notes === Écrire sur son esprit positif. == Aller se confesser == Lundi 4 octobre 2021 [[Image:Artgate Fondazione Cariplo - Molteni Giuseppe, La confessione.jpg|alt=|droite|vignette|upright|<center>Se confesser</center>]] On cherche à se confesser pour savoir où on en est. Se confesser ne vous sera jamais reproché car le secret sera gardé. Par contre il s’agit d’aller se confesser à une autre paroisse parce que le jugement est malheureusement humain. On se confesse pour ne pas refaire les péchés qu’on a dit. Il s’agit de chercher le chemin qu’on nous a proposé. Si notre péché est grand, on cherche un objectif à long terme pour remplacer notre péché. On se confesse alors pour montrer l’évolution de sa démarche. === Mes Notes === Écrire sur ses remords. == Je Crois aux Lois == Samedi 6 novembre 2021 [[Image:Moisés, Domingo Martínez.jpg|alt=|droite|vignette|upright|<center>Les lois sont dépassées par l'éthique.</center>]] Si vous croyez en la loi sachez que la constitution est la première à ne pas être respectée par certaines lois. Aussi la constitution est alors défaite pour créer la tyrannie. Dans une période de récession les lois servent à défaire les libertés par la peur. Si vous croyez en la peur d’un politicien, sachez qu’il n’y a pas à avoir peur dans un monde ordonné. Dans les faits, ceux qui croient en la loi ne sont pas éthiques, parce que l’éthique va au-delà des lois. Dans une école Freinet, des jugements sont établis par l’éthique pour refaire le règlement intérieur. Donc croire en l’éthique, c’est à dire en quelque chose de supérieur à la loi est préférable. === Mes Notes === Écrire sur son éthique. == Je suis Satisfait == mercredi 20 octobre 2021 La satisfaction d’être humain et d’appartenir à l’église est à appréhender. Être satisfait de soi alors que nous ne sommes pas encore des anges est étrange. Nous ne sommes pas encore quelqu’un d’abouti et nous sommes satisfaits de nous. On peut donc être satisfait d’une partie de soi, mais pas de soi en particulier. Quelqu’un de satisfait sur tout ce qui le fait est irrégulier. L’humain n’est pas parfait. Nous nous sommes spécialisés. Être insatisfait permet en réalité de demander plus à Dieu parce que nous sommes imparfaits. Avoir envie de progresser est donc mieux qu’être satisfait de soi. === Mes Notes === Écrire sur ce qui ne vous plaît pas. == Je doute == samedi 6 novembre 2021 [[Image:De ongelovige Thomas Rijksmuseum SK-A-3908.jpeg|alt=|droite|vignette|upright|<center>Ter Brugghen – Thomas doute.</center>]] Reconnaître qu’on est dans le doute c’est reconnaître qu’on ne sait pas tout. On pose les questions où l’on souhaite des réponses sur papier, puis on regroupe ses questions en thèmes, puis on les fait lire par un prêtre. Le prêtre croit en Dieu par la foi en général, mais on peut croire en Dieu par la science. Donc, si ses questions sont scientifiques, on cherche de soi-même les réponses dans des conférences scientifiques créées par des croyants. Le libéralisme empêche de voir la société. Alors on croit les films libéraux, faisant croire que l’individu fait la société, alors que c’est la société qui fait l’individu. En effet, notre ego est créé par notre famille. Quand on est dans le doute, on s’intéresse donc à la société. On agit pour la société. === Mes Notes === Écrire sur ses doutes. == Je Suis dans les Ténèbres == Lundi 4 octobre 2021 [[Image:Emil Brack - The Discussion.jpg|alt=|droite|vignette|upright|<center>Émil Brack – La Discussion</center>]] Il s’agit de se protéger. Être dans l’esprit c’est mieux qu’être dans les émotions. Donc il s’agit de se protéger de ceux qui sont dans les émotions. Ils peuvent devenir malsain facilement. Peu d’amis osent dire la vérité sur vous. Ce sont plutôt des copains. Alors créez votre ami fidèle en étant éthique et moral avec lui. Osez lui dire ce qui peut lui être reproché. Alors on trouve la lumière. Un ami qui n’est pas dans le secret avec son péché n’est pas un ami. Son ami peut hésiter à dire la vérité. Il faut apprendre à dire la vérité à son ami pour qu’il aille vers la vérité. On peut pousser à dire la vérité en cherchant vérité dans la confiance avec son ami. On est franc alors avec son ami. Il faut d’abord supprimer les clichés sur la vérité avec son ami. Par exemple, la vérité est bonne à dire mais elle fait mal. Il ne s’agit pas d’aller contre les autres. Ce sont les personnes malsaines qui font ça. On cherche pourquoi on pense contre et on évite ce qui nous met dans cet état. Alors on trouve la lumière de Dieu avec son ami, celle qui défait nos ténèbres. La lumière qui nous mène n’est pas celle des ténèbres. On se créera un fondement qui nous renforcera. === Mes Notes === Chercher à se sortir des ténèbres en se créant un ami. == Je Suis Radin == Dimanche 7 novembre 2021 [[Image:Charles Dullin, L'Avare, 1944.jpg|alt=|droite|vignette|upright|<center>Charles Dullin en 1944, jouant L'Avare de Molière.</center>]] Être radin n’est pas un comportement utile dans l’univers de Dieu. Dieu veut que nos fruits servent à quelque chose. Dieu veut que les dirigeants servent les autres. L’économie fonctionne par le mouvement, pas par la stagnation de la monnaie. L’argent peut être vite volée. Si votre argent sert à porter des fruits pour les autres, alors vous êtes béni. Seulement, accumuler et profiter de sa situation c’est stagner. Le monde de Dieu est créé par les justes. Seulement les riches auront beaucoup de mal à atteindre le monde de Dieu. Si on est radin pour ses enfants pourquoi pas. Si on ne veut pas prendre de risques pourquoi pas. Seulement cumuler ne sert à rien dans un monde en mouvement. Donc oser prendre des risques est nécessaire au bout d’un moment. === Mes Notes === Écrire sur le mouvement de la vie. = Chercher sa Vérité = En cherchant la vérité on cherche surtout sa vérité avec les autres. == Je ne Valide pas la Bible == mercredi 6 octobre 2021 [[Image:Adolphe-monticelli-ladies-in-elegant-dress-in-a-park.jpg|alt=|droite|vignette|upright|<center>Adolphe Monticelli – On pense que les femmes construisent.</center>]] Tout ne vient pas de Dieu dans la Bible. La Génèse a été écrite 700 ans après la mort de Jésus Christ. L’histoire de Moïse ne tient pas debout. La Génèse dit cependant beaucoup de vérités. Mais elle omet la Maât des africains. La Maât des africains ordonne le monde des humains. Nous avons nous l’Esprit Saint, Jésus et Dieu. Seulement la Maât chez les africains c’est la femme. Donc les africains honorent la femme. Effectivement, si on n’écoute pas celle qui met au monde les enfants, on va vers la guerre. Discutez donc de ce que vous ne validez pas dans la Bible. Seulement, la Maât est peut-être encore plus forte que des écrits de la Bible. Lisez les livres de la librairie Tamery Sematawy Maât. === Mes Notes === Écrire sur sa maman ou sa femme. == Traverser une Épreuve == Mardi 5 octobre 2021 [[Image:Christ Falling on the Way to Calvary - Raphael.jpg|alt=|droite|vignette|upright|<center>Christ tombant - Raphaël.</center>]] Les échecs permettent d’éveiller les sens. On se dit qu’on sera plus fort après cette épreuve. Les sens éveillés permettent d’être plus conscient de l’épreuve. Seulement ils peuvent déstabiliser parce qu’on n’est pas habitué à l’épreuve. Il s’agit de dialoguer avec ses amis pour trouver un chemin qui permette d’affronter cette épreuve. L’épreuve ne s’oubliera pas. On écrit donc sur cette épreuve. Plus tard, on pourra reprendre les écrits pour mieux anticiper. On peut s’en prendre à Dieu. Seulement, l’esprit n’aide-t-il pas lorsque nous allons bien ? Soit on renforcera sa croyance, soit on la fuira. Seulement nous ne pouvons pas fuir l’épreuve. Il ne s’agit pas de comparer sa situation avec une meilleure situation. Il s’agit de comprendre cette situation pour mieux la confronter et la réparer. Il s’agit d’affronter l’épreuve, pas de la laisser passer. On ne se laisse pas écraser. Il s’agit d’être honnête avec soi-même et avec les autres. Si on pense que c’est une injustice, crier à l’injustice avec un ami de confiance. Attention, un juge se sert de tout ce qu’on dit. Il s’agit de préparer sa parole face à un juge, de répéter cette parole avec ses amis. L’honnêteté doit prévaloir sans aller trop loin dans les paroles. Il s’agit d’agir avec discernement. On écrit les solutions à son épreuve. On les compare et on en parle avec ses amis. On peut se dire que le monde nous dépasse, que nous ne sommes qu’un être qui subit une épreuve qui passera sûrement un jour si on est juste. === Mes Notes === Écrire sur un échec en essayant d’améliorer ce passage. == Ma Famille n’est pas Religieuse == jeudi 14 octobre 2021 Difficile de convaincre en dehors de sa famille si celle-ci n’adhère pas à sa religion. Cherchons cependant ceux qui écoutent le mieux notre parole. C’est dans l’adversité qu’on arrive le mieux à convaincre. Seulement nous aurons besoin de réconfort. [[Image:William-Adolphe Bouguereau (1825-1905) - Song of the Angels (1881).jpg|alt=|droite|vignette|upright|<center>William Bouguereau – Le son des anges</center>]] Si on s’est marié avec la mauvaise personne et qu’elle n’adhère pas à sa religion, alors on donne de la confiance et on montre son éthique, sa morale. Si son conjoint est d’une autre religion, alors il s’agit d’échanger. C’est beau d’échanger entre religions. On cherche l’universalité de la croyance dans un Dieu et de la vie éternelle. LirePourquoi un Dieu ? Si son conjoint est athée, il s’agit de se comprendre soi. Si on a choisi un conjoint athée, c’est qu’on était perdu. On aura du mal à convaincre avec un conjoint qui refoule sa religion. Donc, on comprend pourquoi son conjoint refoule la religion. Son conjoint est peut-être chrétien sans le savoir. Alors on loue sa démarche si elle est chrétienne. On se réconforte si on pense que son conjoint va rejoindre Dieu. On cherche à donner la foi à son conjoint s’il en a besoin. Son conjoint doit accepter notre religion. Il ne doit pas la refouler. Donc on se met d’accord sur la frontière des débats religieux et athées. La morale doit être respectée au mieux par l’éthique, c’est à dire aller au devant de la morale. On est encore plus éthique avec un conjoint athée. === Mes Notes === Écrire sur ceux qui ne croient pas en un Dieu. == Chercher sa Vérité == lundi 27 septembre 2021 Avoir la foi demande un chemin de vérité. Tant qu’on n’a pas réponse à nos questions, nos questions peuvent revenir, que ce soit sous forme d’affirmations ou de questions. Ce sont les scientifiques militants qui peuvent répondre à beaucoup de questions. Seulement la réponse peut ne pas plaire. Au début, on ne trouve pas immédiatement les réponses aux questions qu’on nous pose. On se dit alors qu’on trouvera réponses en cherchant sa vérité. Seulement il faut aussi chercher la vérité avec les autres pour trouver sa vérité. [[Image:Friant La Discussion politique.jpg|alt=|droite|vignette|upright|<center>La discussion politique – Émile Friant</center>]] Ce sont les autres qui permettent de nous connaître donc travailler sa communication avec les autres est important, surtout avec l’école qui nous empêche souvent de communiquer en restant à écouter un professeur. La vérité des autres fait plaisir en général. Mais sa vérité peut faire peur ou faire mal. La vérité gêne l’humain en général. Quand son égoïsme se révèle, c’est peut-être qu’une vérité sur soi gêne. Méditer permet de grandir avec une vérité qui nous a gêné. Quand on a découvert sa vérité, on a pris peur. On aimerait ne pas voir cette vérité, l’oublier, constater que les autres n’en parlent pas. Puis on refoule cette vérité. Puis on est humble avec soi-même en y repensant. Alors on s’ouvre aux autres. On mène une nouvelle vie d’évangile en voyant qu’on est pécheur. On est libre et on a peur d'être libre. On trouve alors de nouveaux repères. Certains peuvent prendre une mauvaise voie à ce moment, ne pas vouloir être libre. Alors, si on veut rester libre de sa vérité, les autres nous disent qu'on a changé. On aimerait que les autres voient leur vérité. Donc on milite pour comprendre les autres afin de leur apporter leur vérité, la vérité douce, celle qui est proche de nous. Puis on trouve la vérité qui fait l'humain à un moment donné ou d’autres vérités sur soi. On l'aurait vue autrement sans savoir sa vérité. Ça peut être une vérité dite par un prophète, une vérité que l'on trouve, ou une nouvelle vérité sur soi qu’on accepte bien cette fois-ci. Alors on a envie que les autres comprennent cette vérité qu'ils ne voient pas. Il s’agit de comprendre qu’on n’y croyait pas et qu’on y a cru par un chemin. Il s’agit de demander si on gêne l’autre quand on pense à sa vérité. On pense pourtant qu’on est dans la vérité alors qu’on est dans sa vérité. Les pauvres nous donnent la situation du monde. Nos amis connaissent nos défauts et nous donnent en secret les efforts à faire pour avancer. On peut avoir des copains au travail, rarement des amis. === Mes Notes === Écrire sur ce que l’on ne sait pas. = Chercher la Vérité = Être dans la vérité peut faire de nous un prophète. C’est pour cela qu’on cherche sa vérité et la vérité avec son cœur. On franchit des étapes de vérités sur soi-même et les autres. On médite puis on accepte la vérité envisagée. Alors on grandit et on assimile la vérité. Puis on pourra atteindre la vérité du monde à un instant donné. == Lire la Bible == samedi 16 octobre 2021 [[Image:Amile-Ursule Guillebaud - Interior with elderly woman reading the Bible.jpg|alt=|droite|vignette|upright|<center>Amile-Ursule Guillebaud – Lire la Bible.</center>]] Il existe la catéchèse ou les parcours Alpha pour lire la Bible. Contactez votre paroisse pour rejoindre votre groupe de lecture. La Bible ne se comprend pas entièrement. Elle signifie beaucoup de choses en même temps. Lire seul la Bible se fait après avoir suivi un groupe de lecture. L’ancien testament raconte une période de l’humain qui est passée. Dieu ne se défait plus des humains qui ne le suivent pas. Le nouveau testament est actuel mais reprend une partie de l’ancien testament pour le confirmer. Si vous voulez lire la Bible sans groupe de lecture, commencez par le nouveau testament, puis lisez l’ancien testament en sachant que c’est une période passée. === Mes Notes === Écrire sur ce qui vous a plu dans le nouveau testament. == Avec les Malades == vendredi 5 novembre 2021 [[Image:PeinturesMuséeFabre048.jpg|alt=|droite|vignette|upright|<center>Pierre Dulin – Les malades veulent guérir.</center>]] Les malades ont une toute autre vision de la vie. Ils la voient comme cruciale et fragile. Ceux qui ont été tout le temps malades ont donc une vie extraordinaire à comprendre. On se prépare alors à voir ce qu’on ne pensait pas voir. Seulement, les plaintes du manque de considération peuvent jaillir. Alors on se confesse avec le malade pour le révéler à sa vie extraordinaire. Peut-être s’ouvrira-t-il à nous. L’art de se détacher du monde est alors à prendre en compte. Profiter du moment présent est alors à envisager. En réalité, les malades sont comme vous et moi. Ils ont des habitudes qui ne sont pas à la mode. Seulement, ils ont une meilleure appréhension de la vie parce qu’ils observent. === Mes Notes === Écrire sur un moment de maladie. == Devenir Libre == Lundi 27 septembre 2021 Devenir Libre est un désir d’autonomie. Mais devenir libre fait peur. Pour ne pas changer, beaucoup ne veulent pas devenir libre. [[Image:Man Writing a Letter by Gabriël Metsu.jpg|alt=|droite|vignette|upright|<center>Écrire des poèmes rend libre – Gabriel Metsu.</center>]] Chercher la vérité permet de devenir libre. C’est sortir du monde du diable qui fait qu’on devient libre. On se crée un fondement qui nous renforce. On pose les questions qui piègent aux autres, ceux qui savent, le non respect des dogmes de la Bible par exemple, puis on donne les réponses une fois qu’on a trouvé les réponses. On aura été habitué au monde du diable. Donc on voudra inconsciemment y revenir au début de sa liberté. On réfléchit sur la morale par l’éthique. On trouve des réponses à nos questions avec son ou ses amis. On a de quoi devenir responsable de projets, comme connaître la communication et la prise de responsabilités. Si on a une vie de tentations contre la morale, on essaye d’abord de s’en détacher, puis on part à l’aventure d’une nouvelle vie. Il s’agit de choisir entre une vie carriériste ou une vie éthique. Puis on voit les faiblesses avec le monde du diable. On est né de nouveau. Être libre permet alors de devenir heureux avec sa véritable condition, celle qui fait qu’on sortira de son corps, après avoir trouvé plein de fois la vérité. On aura donc été prophète. La mort sera un passage. Jésus peut nous choisir si notre poutre est suffisamment solide. Appeler à l’aide est un signe de liberté. Notre cœur veut devenir libre. Alors on essaie de trouver la bonne personne pour nous aider. === Mes Notes === Vous sentez-vous libre ? == Le Chemin de Vérité == vendredi 24 septembre 2021 Le chemin de vérité est un chemin de progressions et de régressions pour comprendre le monde dans lequel on est. L’aboutissement du chemin est de devenir prophète. Savoir mener sa vie est primordial pour trouver le bon chemin. Il faut savoir se protéger des mauvais chemins pour trouver la vérité. Il faut oser trouver le bon chemin, celui qui dit la vérité de la Bible. Son chemin de vérité permettra de grandir. Au début, nous agissons tous comme des enfants. L’enfance revient même quand on est adulte parce que notre esprit veut rester dans le bonheur de l’enfance. [[Image:Mirabello Cavalori (1535-1572) - A Discussion - NG3941 - National Gallery.jpg|alt=|droite|vignette|upright|<center>Mirabello Cavalori - Une Discussion</center>]] L’hypothèse supérieure qui englobe un ensemble d’idées et qui les défait toutes est difficile à atteindre dans un monde cartésien. Il s’agit de lire Platon, Lyndon Larouche, De Cuse, Leibniz, Riemann, Einstein ou des livres scientifiques de la librairie Tamery Sematawy Maât. C’est une hypothèse supérieure qui nous fera grandir. Nous répondrons à notre vérité par une nouvelle vérité qui fera de nous un adulte. Cela fera peur. On trouvera de nouveaux repères. L’esprit est là pour nous aider, que l’on soit méchant ou gentil. L’esprit veut que nous cherchions la vérité pour révéler une partie de soi et des autres. L’esprit aime les amis qui nous permettent de trouver vérité. N’avons-nous jamais vu des décideurs chercher des conseillers ? Les conseillers sont eux-mêmes conseillés et font le bien ou le mal avec. Ainsi, l’esprit veut nous révéler la vérité, celle du mal si nous cherchons la vérité du mal. Seulement il coupe la vérité aux mauvais décideurs et à leurs conseillers. C’est pour cela que des décideurs se mettent à faire n’importe quoi. Quand on ne cherche rien l’esprit ne nous aide pas. Les chrétiens sont ceux qui cherchent vérité. En effet, nous sommes baptisés pour trouver le Christ et Christ est vérité. Seulement il y a des chrétiens païens qui cherchent à faire le bien parce que cela fait du bien. Les chrétiens non païens ont en général une bonne place dans la société parce qu’apporter du bien a permis aux parents d’être bien vus. Pourquoi voulons-nous le mal ? Parce que le mal existe. Il ne permet pas la vie en société donc Dieu se passe de ceux qui cherchent le mal ou vivre l’instant présent sans apporter aux autres. Par contre si nous voulons savoir pourquoi il y a le mal et le bien, nous trouvons réponses, parce qu’il y a forcément quelqu’un qui a créé tout ça. Dieu ne s’intéresse pas aux biens matériels. Ainsi, vous ne trouverez pas vérité avec des biens matériels que vous avez consommés. Notre esprit est en effet vite diverti par ce que nous lisons. Notre esprit est encore animal et ne voit pas facilement l’envers du décor, parce que l’esprit est fait pour savoir si nous pouvons aimer par la vérité, pas après la vérité. C’est la vérité qui libère. Au début on a peur, puis ensuite on veut en savoir plus. Seulement il s’agit de passer cette peur, de découvrir sa vérité pour chercher la vérité. === Mes Notes === Chercher sa vérité pour aller chercher la vérité. Ceci est une démarche quotidienne. == Convaincre par la Politique == Samedi 25 septembre 2021 Les chrétiens sont souvent persécutés pour leur recherche de vérité. En occident ils ont fait face en 2021 à l’individualisme forcené qui aboutit à tout vouloir faire par soi-même. Un chrétien a des amis qui lui permettent d’avancer dans la démarche de recherche de vérité et lui permettant d’aimer. [[Image:Solidarité et progrès Paris 2015.jpg|alt=|droite|vignette|upright|<center>Des partis politiques militent dans les rues.</center>]] Convaincre avec l’évangile ça peut être s’intéresser à une partie de l’évangile qui est faussée par le discours ambiant. Si une partie de l’évangile semble fausse, alors c’est toute la Bible qui est remise en cause par les païens. Ils vont alors vous embêter avec ce qu’ils trouvent incertain. Mais ils vont vous dire par exemple : « Pourquoi tant de précarité s’il faut se multiplier ? ». En période de récession cette partie de l’évangile est remise en cause alors qu’elle est vraie. On peut toujours se multiplier. L’esprit et la nature permettent cela. Ce qui gêne c’est la cupidité de l’humain. L’argent devient un but en période de récession pour survivre, alors qu’on pourrait se développer. En période de récession, il faut agir en politique pour changer ce paradigme de cupidité. Le chrétien est individualisé par les médias appartenant au diable en 2024. Les médias qui le contredisent le mieux sont diabolisés. Ainsi, il faut s’intéresser à celui qui est peu écouté. Pourquoi cela ? Si des passages de la Bible sont faussés, alors ceux qui défendent ces passages de la Bible ne sont pas écoutés. Il s’agit donc d’écouter ceux qui défendent les phrases contredites de la Bible. Si nous ne croyons pas ces personnes, alors il y a sans doute des personnes cachées qu’il s’agit de découvrir. Il n’y a pas à s’accorder quand on n’est pas d’accord avec quelqu’un. On n’est pas d’accord et chacun le sait. Une vérité permet d’oublier le désaccord. Seulement il faut comprendre ce désaccord donc ne pas l’oublier pour le résoudre. === Mes Notes === Aller voir tous les partis politiques. == J’ai Peur de Convaincre == Samedi 25 septembre 2021 [[Image:Frédéric Bazille 001.jpg|alt=|droite|vignette|upright|<center>Frédéric Bazille - Une réunion</center>]] Si vous avez peur de convaincre c’est soit que vous n’avez pas essayé, soit que vous n’êtes pas assez convaincu, soit que vous n’aimez pas. La recherche de Dieu est perpétuelle parce que nous ne verrons le créateur que dans le monde d’après, si nous sommes prophètes sans doute. Dieu se comprend avec l’ensemble de l’univers, des lois de la physique, des lois du vivant surtout, avec la nature créative humaine, avec l’esprit aussi. On a peur d’essayer parce que l’on se dit qu’on ne convaincra pas. Nous ne sommes pas forcément faits pour la communication. Mais la communication s’apprend. Il s’agit surtout d’aimer. Tout notre univers est une création de Dieu. Lisez le livre '''Pourquoi un Dieu ?''' Pour mieux comprendre. === Mes Notes === Si vous avez peur d’essayer vous pouvez suivre un cours de communication, culture et expression au CNAM ou vous pouvez rejoindre un groupe de théâtre ou mieux, d’improvisation. Chercher Dieu avec les autres. = Être dans la Vérité = Être dans la vérité c’est aussi utiliser son amour. On souhaite que les autres aillent dans le monde de Dieu. Être dans la vérité c’est se réjouir d’avoir une nouvelle vérité humaine, sans penser à la sienne mais en voulant que chacun y aille. On est dans la vérité quand on transmet la Bible, quand on trouve ce qui fait l’humain à un instant donné. Alors on convainc les autres de créer le monde que veut Dieu pour nous. Il est peut-être déjà là sans qu’on le voit == Lire la Bible à un Athée == Lundi 4 octobre 2021 La catéchèse permet aux volontaires d’apprendre la Bible. Il y a aussi les livrels des témoins de Jéhovah qui permettent d’être positif avec la Bible. Lire la Bible avec un adulte athée nécessite de bien connaître la Bible, parce que ce qui va intéresser l’athée peut évoluer dans le temps. [[Image:Jean-Baptiste Greuze - Reading the bible.jpg|alt=|droite|vignette|upright|<center>Lire la Bible c’est mieux à plusieurs.</center>]] Quand quelqu’un donne un péché et que l’on ne l’a pas fait on peut penser qu’il ne va pas aller voir Dieu. Cependant Dieu donne sa miséricorde à quiconque. Il s’agit donc de dire que Dieu pardonne parce que dire son péché c’est un peu se confesser. Il faut aussi se protéger et partir si on est touché. Après on pourra réagir correctement à ce genre de péché. Dans un monde étriqué, il s’agit de faire de la psychologie pour que les personnes qui sont en face de nous se dévoilent. Alors la Bible intervient pour aider. On veut tout savoir. Seulement dire un péché c’est un peut se confesser. Donc ses désirs humains sont à laisser de côté avec le péché. C’est le cœur qui doit parler à ce moment là. Si celui qui dit son péché n’accepte pas le pardon, alors on peut se demander comment il peut rejoindre Dieu. Il s’agit de construire, pas de détruire. Il s’agit de dire ce qu’on ressent, pas de cacher la miséricorde. Sinon vous serez jugé de la même manière que vous jugerez. La Bible doit pouvoir être interprétée avec amour. Elle doit servir à aider, pas à reprocher. Si on voit celui qu’on veut convaincre comme quelqu’un de malsain, alors on peut chercher pour lui la miséricorde ou le bonheur. Il s’agit de lui montrer notre amour, pas de le réprimer. Il s’agit de deviner à qui on a affaire. Si la personne dit des choses malsaines, elle est peut-être honnête avec elle-même. Alors il s’agit de donner des conseils. On ne doit pas accepter ce qui est malsain. Il s’agit de partir quand on se sent influencé ou de trouver ce qui est positif pour couper les paroles malsaines. === Mes Notes === Lire la Bible quand on peut s’en souvenir. == La Bonne Nouvelle, L’Évangile == Samedi 9 octobre 2021 [[Image:Leonardo da Vinci (1452-1519) - The Last Supper (1495-1498).jpg|alt=|droite|vignette|upright|<center>Léonard de Vinci, le dernier dîner</center>]] L’évangile nous appelle à quelque chose de nouveau. Donc elle montre qu’on s’adapte aux autres pour la bonne nouvelle, qu’il y a une vie après la mort. Si on ne croit pas qu’il y a la vie après la mort, on peut regarder les témoignages de mort imminente, où des personnes quittent leur corps et voient différemment les choses ensuite. Si on ne croit pas à la vie après la mort, on est peut-être dans un individualisme qui nous empêche de voir les autres. La vie est un miracle. Notre individualisme conforté par le libéralisme nous empêche de voir cela. Alors on s’intéresse aux films sociaux peu nombreux. On s’intéresse aussi aux maîtres qui ont combattu le libéralisme pour permettre des booms économiques, maîtres cités dans les autres livrels. La Bonne Nouvelle est un objectif à comprendre. On ne comprend pas tout dans la bonne nouvelle tout de suite. Jésus disait beaucoup de choses dans ses paroles. C’est pourquoi il s’agit de parler de sa bonne nouvelle, de cette bonne nouvelle qui nous mène. On passera par des épreuves, dirigées par le diable certaines fois. On peut éviter certaines épreuves. Il faut chercher une sincérité, c’est à dire une compréhension de soi et des autres avec ces épreuves. === Mes Notes === Écrire sur les paroles de Jésus. == Révéler son Chemin == Samedi 13 novembre 2021 [[Image:Norman Rockwell - Fishing Trip, They'll Be Coming Back Next Week - Google Art Project.jpg|alt=|droite|vignette|upright|<center>Son chemin est celui des autres - Norman Rockwell.</center>]] Votre démarche de recherche de vérité est importante. Ceux qui discutent avec vous veulent surtout savoir pourquoi vous croyez en Dieu. Ainsi votre chemin est à donner aux autres. Cela peut même donner un chemin à suivre aux autres. On a à faire des choix pour plus tard et vous donnez un chemin possible. Donc n’hésitez pas à montrer votre chemin de vérité. Seulement, la foi est encore plus importante pour transmettre. Donc ce chemin de vérité a fini par donner la foi. Les étapes qui vous ont permis de croire en Dieu sont cependant importantes. Ne pas avoir cru en Dieu est peut-être une étape à donner. === Mes Notes === Écrire sur son chemin de vérité sur Dieu. == Devenir Saint == Lundi 27 septembre 2021 [[Image:Église Saint-Martin de Castelnau-d'Estrétefonds - La Prédication de saint Jean-Baptiste par Robert Arsène IM31000072.jpg|alt=|droite|vignette|upright|<center>La Prédication de saint Jean-Baptiste - Robert Arsène</center>]] Dieu seul est saint. Seulement nous sommes appelés à devenir saint. Si nous le sommes un instant, c’est déjà un grand chemin de parcouru. Pour devenir saint à certains instants, on cherche la vérité avec les autres pour agir avec les autres pour un monde nouveau, le monde que veut la Bible. Alors on devient prophète ou saint un instant. On dit aussi qu’on est saint quand on est accepté dans le monde de Dieu. En réalité on est juste. Le monde de Dieu est très accessible en réalité. C’est plutôt que l’humain a tendance à suivre le diable parce qu’enfermé dans son monde. C’est pour cela qu’il faut parler du monde de Dieu. Un juste devrait pouvoir parler de lui-même de la façon que Dieu le voit. On écrit des poèmes. === Mes Notes === Se sent-on juste ? == Porter sa Croix == Dimanche 26 septembre 2021 Porter sa croix permet de défaire son orgueil. En effet, ne connaître que la bienveillance fait de nous un roi qui veut tout, un enfant gâté en quelque sorte. Dire la vérité embête les autres. Ils ne peuvent pas accepter d’être dans le mensonge. Ainsi ils refoulent ceux qui disent la vérité en fonction de clichés ou de fausses raisons. Beaucoup sont dans le mensonge en période de récession. Donc les chrétiens qui disent la vérité, celle qui permet d’avancer surtout, sont refoulés. Puis leur parole agit sur certains. Pour porter sa croix il faut avoir vu la vérité et avoir été convaincu par cette vérité qui englobe le monde à un instant donné. Alors on répète cette vérité qui fait avancer le monde pour se voir rabaissé par des personnes dans le mensonge. Alors l’esprit et les conseils de son entourage nous aident à trouver la bonne voie. [[Image:Augustins - Le Christ en croix avec l'orant du cardinal Guilhem Peire Godin - 49 6 15.jpg|alt=|droite|vignette|upright|<center>La croix rappelle un chemin à suivre.</center>]] Il s’agit donc de connaître ceux qui sont dans le mensonge pour pouvoir apporter un chemin qui permette de sortir du mensonge. Le mensonge c’est vouloir continuer à vivre au jour le jour en pensant qu’on ne vit qu’une fois. Tout va pour cela dans une période de récession. Il s’agit de montrer l’intérêt général qui permet de nouveaux droits. L’intérêt général nécessite une éthique personnelle. Ainsi on voit sur le long terme pour se créer de nouveaux droits. Pour aller contre le mensonge qui dit que l’humain est mauvais, il s’agit de montrer sa bonté et d’expliquer pourquoi un comportement inadapté se passe. Par exemple beaucoup jettent les déchets dans la poubelle. Seulement nos états envoient des déchets en Afrique. L’humain est bon quand il naît mais une mauvaise société le rend mauvais. Aller contre la société est dangereux. Donc il s’agit de trouver un groupement politique qui aille vers la société idéale. Ces groupements politiques existent. Mais ils sont minoritaires en période de récession. La société idéale est morale et éthique. Donc il s’agit de trouver des personnes morales et éthiques. Porter sa croix doit servir à quelque chose, parce que l’on sera refoulé. Porter sa croix nécessite d’être dans la communication qu’on utilise. On aura développé son intelligence inter-personnelle en cherchant la vérité avant de porter sa croix avec ses amis. Porter sa croix c’est dire la vérité sans qu’elle soit accomplie. Par contre on est soi-même accompli. Si on est un prophète, cette vérité s’accomplira plus tard. Alors on trouve quelqu’un qui écoute. On aura peut-être trouvé un nouvel ami. Se souviendra-t-on de celui qui écoute ou de ceux qui n’ont pas écouté ? === Mes Notes === Écrire sur la passion de Jésus Christ. == Naître de Nouveau == Dimanche 26 septembre [[Image:Lutheran baptism.jpg|alt=|droite|vignette|upright|<center>Un baptême</center>]] Ce sera souvent après un gros échec qu’on naîtra de nouveau. Ou bien on aura trouvé un philosophe qui nous aura pris aux tripes. On aura surtout eu un ami nous ouvrant l’esprit. Quand on naît de nouveau, on voit toute la mauvaise société comme un gigantesque péché. On n’accepte plus la mauvaise voie et on la voit en fonction de ses capacités. On est alors dégoûté par la mauvaise société. Il s’agit alors de construire la bonne société avec ses amis. Il s’agit de trouver ce qui permettra la nouvelle société. Elle est là avec les gens qui sont autour de nous. Puis on s’adapte à cette mauvaise société. Donc on connaît cette mauvaise société. Seulement notre éthique reste là parce que nous avons découvert la vraie société. On convainc d’une nouvelle société. On pourrait écrire de beaux poèmes sur sa nouvelle naissance. Les poèmes auront déjà permis de voir nos défauts. === Mes Notes === Écrire sur son adolescence. == Prêcher l’Évangile == mardi 5 octobre 2021 On prêche l’évangile pour mieux l’assimiler. Seulement on sait déjà l’évangile. On ne sait pas par contre à quels points elle est vraie. L’ancien testament est plus douloureux que le nouveau testament. Être dans le monde de Dieu est possible. Donc prêchons le nouveau testament. Après avoir milité ou prêché on sera dans la vérité si on essaye de convaincre les autres. Donc cherchons la vérité avec les autres. La semence de sa parole porte des fruits si nous unissons l’esprit avec lequel nous parlons. Beaucoup sont divisés. === Mes Notes === Écrire sur le nouveau testament. == Sur Internet == Mercredi 6 octobre 2021 Internet c’est un va-et-vient. Regarder les directs permet de souvent commenter. Il ne faut pas hésiter à commenter les directs scientifiques pour montrer la science de l’église. Les directs politiques demandent souvent de l’espérance. Donner ses bonnes nouvelles qui contredisent le diable permet de faire réfléchir. Créer une carte de visite avec des références gratuites de vidéos ou de livrels gratuits permet de donner de l’espérance. === Mes Notes === S’intéresser aux vlogs scientifiques et politiques. = Prophétiser = Deviner l’avenir permet de prophétiser. Ainsi, connaître l’économie réelle permet de prophétiser. == Révéler Dieu Scientifiquement == mercredi 6 octobre [[Image:Hubble Sees a Bizarre Cosmic Rarity (11241869426).jpg|alt=|droite|vignette|upright|<center>Une rareté cosmique</center>]] Il ne s’agit pas de parler directement de Dieu, mais de dire que l’on vient de Dieu et comment. Dieu serait la première vie et Dieu nous a créé créatifs comme lui pour parler avec des amis, les justes. Les âmes ne se voient pas dans notre monde. Pourtant, on peut les deviner parce que la matière ne peut pas s’organiser toute seule. Il faut une intervention d’un autre univers pour que la matière s’ordonne. Jésus est le sauveur. Les preuves des paroles de Jésus sont incontestables. Je pense qu’on sera avec Jésus Christ après l’apocalypse. Qu’en pensez-vous ? === Mes Notes === Lisez '''L’Univers est Vivant !''' == Être dans l’Esprit == Jeudi 14 octobre 2021 [[Image:Quirijn van Brekelenkam - Interior with Cardplayers.jpg|alt=|droite|vignette|upright|<center>Une discussion - Quirijn van Brekelenkam</center>]] Pour être dans l’esprit, il faut avoir la foi. Des athées peuvent avoir la foi et suivre l’esprit, parler avec son intuition en allant de l’avant. L’autre nous dit quelque chose et on parle avec l’esprit par rapport à ce qu’on pense de lui. On ne sait pas ce qu’on va dire à l’instant, mais l’esprit nous mène pour parler à l’autre. Si on est dans l’esprit saint, on est subjugué par ce qu’on a dit. Puis, plus tard, on se demande comment on a pu dire ça. Ça peut aussi être une hypothèse supérieure qui vient quand on réfléchit, c’est à dire quand on confronte les idées dans la tête en méditant. Alors cette idée défait certaines de nos idées pour les refaire sous une autre forme plus tard. Si on n’a pas la foi, on n’arrive pas à être dans l’esprit. C’est après avoir répondu à ses questions, sa recherche de vérité, qu’on peut parler dans l’esprit. Comme c’est beau de garder l’enfance de son premier amour pour Dieu. Garder la fraîcheur le plus longtemps possible est important. Après on sera sage. === Mes Notes === Essayer d’être mené par l’esprit quand on parle. == Devenir Prophète == mardi 28 septembre 2021 [[Image:Giorgio Vasari - The Prophet Elisha - WGA24289.jpg|alt=|droite|vignette|upright|<center>Le prophète Élie - Giorgio Vasari</center>]] Le prophète intervient quand il y a récession. Il permet de diffuser le vrai message de la Bible, celui que la finance contredit dans la Bible. Il peut ajouter de nouveaux préceptes. Pour devenir prophète, il faut avoir une intelligence inter ou intra personnelle forte. Il s’agit de savoir communiquer par écrit grâce à Internet ou par oral. Un prophète a surtout la capacité d’apprendre. Il a forcément développé ses intelligences grâce aux autres. Le prophète a surtout beaucoup de liens avec la société lui permettant de ne pas être tué. Alors le Christ peut l’aider. Si l’esprit est entièrement dépendant de votre vérité pendant plusieurs, vous ne dormez pas pendant plusieurs jours sans que cela fatigue votre corps. Aussi vous aurez toujours le bilan de l’intuition de tous les humains à 500 mètres autour de vous. Alors l’esprit, l’esprit saint ou le Christ, voire Dieu, aident le prophète à faire face au monde par la vérité du prophète englobant le monde. === Mes Notes === Essayer de militer pour convaincre les autres du monde de Dieu ou d’une économie humaine. == La Parole de Connaissance == lundi 18 octobre 2021 La parole de connaissance est une connaissance sur le plan de Dieu qui va se passer. La parole de connaissance n’est pas une idée qui passe par la tête. On est sûr dans son esprit qu’elle va se produire. La parole de connaissance n’est pas à confondre avec l’économie. On peut deviner ce qui va se passer dans la société avec l’économie. === Mes Notes === Ai-je déjà été sûr qu’un événement se produise ? == La Parole de Sagesse == mercredi 20 octobre 2021 La parole de sagesse est une révélation surnaturelle donnée par Dieu. Cela donne une nouvelle vérité sur l’humain qui arrivera peut-être plus tard, distribuée par un prophète. On ne peut distinguer facilement la parole de sagesse de la vérité scientifique qu’avec la partie surnaturelle de la parole de sagesse. C’est quelqu’un qui est dans la vérité qui reçoit la parole de sagesse. Il n’a pas à être plus intelligent que les autres pour être dans la vérité. === Mes Notes === Écrire sur le surnaturel de la Bible. {{AutoCat}} gc56ylq8xblucjq9845285va4cpxnv1 Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements 0 78310 763752 717648 2026-04-16T13:32:18Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Le contrôleur de périphériques]] vers [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements]] : Réutilisation d'un chapitre vide, vers un nouveau sujet 717648 wikitext text/x-wiki <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les méthodes de synchronisation entre processeur et périphériques | prevText=Les méthodes de synchronisation entre processeur et périphériques | next=L'adressage des périphériques | nextText=L'adressage des périphériques }} </noinclude> f8hssje6l6jb05dy9afsryupwfoplre 763754 763752 2026-04-16T13:32:58Z Mewtow 31375 763754 wikitext text/x-wiki L'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le ''program counter'' quand un branchement est détecté. L'altération du ''program counter'' est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste. ==L'implémentation des branchements conditionnels== Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels : * Ceux qui doivent être précédés d'une instruction de test ou de comparaison. * Ceux qui effectuent le test et le branchement en une seule instruction machine. * Ceux où les branchements conditionnels sont émulés par une ''skip instruction'', une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels.]] Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué. ===Les circuits de saut conditionnel et de calcul de la condition=== Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les '''bits intermédiaires''', et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs. [[File:Calcul d'une condition pour un branchement.png|centre|vignette|upright=1|Calcul d'une condition pour un branchement]] Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du ''program counter'' et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le ''program counter'' ou non. Le ''program counter'' doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant. [[File:Implémentation des branchements conditionnels dans le séquenceur.png|centre|vignette|upright=2|Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.]] Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple. [[File:Implémentation des branchements avec plusieurs conditions.png|centre|vignette|upright=2|Implémentation des branchements.]] Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante. ===Le registre d'état ou les registres à prédicats et les circuits associés=== Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières. * La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat. * La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement. Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change. Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul. [[File:Implémentation des branchements avec un registre d'état.png|centre|vignette|upright=2|Implémentation des branchements avec un registre d'état]] L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur. [[File:Implémentation de l'unité de chargement avec plusieurs registres à prédicats.png|centre|vignette|upright=2|Implémentation de l'unité de chargement avec plusieurs registres à prédicats]] ==L'implémentation des ''skip instructions''== Passons maintenant au cas des ''skip instruction'', qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une ''skip instruction'' permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le ''program counter'' est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition. [[File:Implémentation des branchements pseudo-conditionnels dans le séquenceur.png|centre|vignette|upright=1.5|Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.]] ==L'implémentation des instructions à prédicats== Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide. La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles. [[File:Implémentation des instructions à prédicats.png|centre|vignette|upright=2|Implémentation des instructions à prédicats]] La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions. [[File:Implémentation des instructions à prédicats simples.png|centre|vignette|upright=2|Implémentation des instructions à prédicats simples]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les méthodes de synchronisation entre processeur et périphériques | prevText=Les méthodes de synchronisation entre processeur et périphériques | next=L'adressage des périphériques | nextText=L'adressage des périphériques }} </noinclude> mcyekbmy4jbr8crs17epj80odqy8xk0 763757 763754 2026-04-16T13:34:15Z Mewtow 31375 /* L'implémentation des instructions à prédicats */ 763757 wikitext text/x-wiki L'implémentation des branchements implique tout le séquenceur et l'unité de chargement. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le ''program counter'' quand un branchement est détecté. L'altération du ''program counter'' est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste. ==L'implémentation des branchements conditionnels== Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels : * Ceux qui doivent être précédés d'une instruction de test ou de comparaison. * Ceux qui effectuent le test et le branchement en une seule instruction machine. * Ceux où les branchements conditionnels sont émulés par une ''skip instruction'', une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels.]] Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué. ===Les circuits de saut conditionnel et de calcul de la condition=== Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les '''bits intermédiaires''', et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs. [[File:Calcul d'une condition pour un branchement.png|centre|vignette|upright=1|Calcul d'une condition pour un branchement]] Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du ''program counter'' et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le ''program counter'' ou non. Le ''program counter'' doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant. [[File:Implémentation des branchements conditionnels dans le séquenceur.png|centre|vignette|upright=2|Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.]] Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple. [[File:Implémentation des branchements avec plusieurs conditions.png|centre|vignette|upright=2|Implémentation des branchements.]] Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante. ===Le registre d'état ou les registres à prédicats et les circuits associés=== Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières. * La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat. * La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement. Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change. Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul. [[File:Implémentation des branchements avec un registre d'état.png|centre|vignette|upright=2|Implémentation des branchements avec un registre d'état]] L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur. [[File:Implémentation de l'unité de chargement avec plusieurs registres à prédicats.png|centre|vignette|upright=2|Implémentation de l'unité de chargement avec plusieurs registres à prédicats]] ==L'implémentation des ''skip instructions''== Passons maintenant au cas des ''skip instruction'', qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une ''skip instruction'' permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le ''program counter'' est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition. [[File:Implémentation des branchements pseudo-conditionnels dans le séquenceur.png|centre|vignette|upright=1.5|Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.]] ==L'implémentation des instructions à prédicats== Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide. La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles. [[File:Implémentation des instructions à prédicats.png|centre|vignette|upright=2|Implémentation des instructions à prédicats]] La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions. [[File:Implémentation des instructions à prédicats simples.png|centre|vignette|upright=2|Implémentation des instructions à prédicats simples]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les méthodes de synchronisation entre processeur et périphériques | prevText=Les méthodes de synchronisation entre processeur et périphériques | next=Les architectures à accumulateur | nextText=Les architectures à accumulateur }} </noinclude> cj8gam9ccq9w24q1qb7xscsg8giy06g 763759 763757 2026-04-16T13:39:53Z Mewtow 31375 763759 wikitext text/x-wiki L'implémentation des branchements implique tout le séquenceur, l'unité de chargement, et même les unités de calcul. L'implémentation des branchements demande que l'on puisse identifier les branchements, et altérer le ''program counter'' quand un branchement l'impose. L'altération du ''program counter'' est le fait de l'unité de chargement. Elle a juste besoin qu'on lui précise à quelle adresse brancher, et quand un branchement a lieu. Quant au séquenceur, il doit gérer tout le reste. Pour les branchements conditionnels, il faut aussi faire une comparaison dans l'ALU et envoyer son résultat au séquenceur. Vu que les branchements impliquent tout le processeur, chemin de données inclu, nous ne pouvons pas en parler dans le chapitre sur l'unité de chargement, ni sur l'unité de contrôle, ni sur le chemin de données. Il est préférable de voir ceux-ci dans un chapitre à part, séparé des trois précédents. C'est pour cela que nous avons séparé ce chapitre des autres chapitres sur la microarchitecture d'un processeur. Nous allons y aborder les branchements, conditionnels comme inconditionnels, mais aussi les instructions à prédicats et les instructions ''skip''. Elles ont des implémentations assez différentes, mais qui ont la même particularité : elles impliquent tout le processeur. ==L'implémentation des branchements conditionnels== Les branchements inconditionnels sont les plus simples à gérer. Il suffit de détecter si une instruction est un branchement inconditionnel, et de déterminer où se trouve l'adresse de destination. Pour cela, on doit ajouter un circuit de détection des branchements, qui détecte si l'instruction exécutée est un branchement ou non. Il est situé dans le décodeur d'instruction. La détermination de l'adresse dépend du mode d'adressage et implique de configurer correctement le chemin de données. Il y a peu çà dire Par contre, les branchements conditionnels demandent en plus de vérifier qu'une condition est respectée, ils demandent de faire calculer une condition pour savoir s'il faut faire le saut. Sur les jeux d'instruction modernes, tout est fait en une seule instruction : le branchement calcule la condition en plus de faire le saut. Mais les jeux d'instruction anciens séparaient le calcul de la condition et le branchement dans deux instructions séparées, ce qui demande d'ajouter un registre pour faire le lien entre les deux. L'instruction de test doit fournir un résultat, qui est mémorisé dans un registre adéquat. Puis, le branchement lit ce registre, et décide de sauter ou non. Pour rappel, il existe trois types de branchements conditionnels : * Ceux qui doivent être précédés d'une instruction de test ou de comparaison. * Ceux qui effectuent le test et le branchement en une seule instruction machine. * Ceux où les branchements conditionnels sont émulés par une ''skip instruction'', une instruction de test spéciale qui permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. [[File:Implémentations possibles des branchements conditionnels.png|centre|vignette|upright=3|Implémentations possibles des branchements conditionnels.]] Formellement, un branchement conditionnel demande de faire deux choses : calculer une condition, puis faire le branchement suivant le résultat de la condition. Dans ce qui suit, nous allons d'abord voir le cas où calcul de la condition et saut conditionnels sont réalisés tous deux par une seule instruction. Puis, nous verrons ensuite le cas où test et saut sont séparés dans deux instructions séparées. La raison est que le premier cas est le plus simple à implémenter. Le second cas demande d'ajouter des registres et quelques circuits, ce qui rend le tout plus compliqué. ===Les circuits de saut conditionnel et de calcul de la condition=== Le calcul de la condition adéquate est réalisé par un circuit assez simple, qui est partagé entre le séquenceur et le chemin de données. Premièrement, deux opérandes sont lus depuis les registres, puis sont envoyés à un circuit soustracteur qui soustrait les deux opérandes. Le résultat de la soustraction n'est pas mémorisé dans les registres, mais quelques portes logiques extraient des informations importantes de ce résultat. Notamment, elles vérifient si : le résultat est nul, le résultat est positif/négatif, si la soustraction a entrainé un débordement entier signé, ou un débordement non-signé (une retenue sortante). Ces quatre résultats sont appelés les '''bits intermédiaires''', et ils sont combinés pour calculer les différentes conditions. En combinant les quatre résultats, on peut déterminer toutes les conditions possibles : si les deux opérandes sont égaux, si la première est inférieure/supérieure à la seconde, etc. Toutes les conditions sont calculées en parallèle et la bonne est alors choisie par un multiplexeur commandé par le séquenceur. Au passage, nous avions déjà vu ce circuit dans le chapitre sur les comparateurs, dans la section sur les comparateurs-soustracteurs. [[File:Calcul d'une condition pour un branchement.png|centre|vignette|upright=1|Calcul d'une condition pour un branchement]] Outre le calcul de la condition, un branchement conditionnel saute ou non à une certaine adresse. On sait déjà que le saut s'effectue en présentant l'adresse de destination sur l'entrée adéquate du ''program counter'' et en mettant à 1 son entrée de réinitialisation. La seule difficulté est de décider s'il faut mettre à jour le ''program counter'' ou non. Le ''program counter'' doit être réinitialisé dans deux cas : soit on a un branchement inconditionnel, soit on a un branchement conditionnel ET que la condition est respectée. Détecter si la condition est respectée est assez simple : elle est dans un registre à prédicat, ou calculé à partir du registre d'état, comme vu plus haut. Reste à identifier les branchements et leur type. Pour cela, le séquenceur dispose de circuits qui détectent si l'instruction chargée est un branchement conditionnel ou inconditionnel. Ces circuits fournissent deux bits : un bit qui indique si l’instruction est un branchement conditionnel ou non, et un bit qui indique si l’instruction est un branchement inconditionnel ou non. Il reste alors à combiner ces deux bits avec le résultat de la condition, ce qui se fait avec quelques portes logiques. Le circuit final est le suivant. [[File:Implémentation des branchements conditionnels dans le séquenceur.png|centre|vignette|upright=2|Implémentation des branchements conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici.]] Effectuer un branchement demande donc de combiner les deux circuits précédents, en mettant le second à la suite du premier. Le schéma ci-dessous montre ce qui se passe quand test et saut sont fusionnés en une seule instruction, où il n'y a pas de séparation entre instruction de test et branchement. Le circuit ci-dessous est le plus simple. [[File:Implémentation des branchements avec plusieurs conditions.png|centre|vignette|upright=2|Implémentation des branchements.]] Avec une séparation entre test et branchement, les choses sont plus compliquées, car l'ajout de registres à prédicats ou d'un registre d'état complexifie le circuit. Et c'est ce que nous allons voir dans la section suivante. ===Le registre d'état ou les registres à prédicats et les circuits associés=== Voyons maintenant ce qui se passe quand on sépare le branchement en deux, avec une instruction de test séparée des branchements conditionnels. La répartition des tâches entre instruction de test et branchement conditionnel est assez variable suivant le processeur. Pour rappel, on peut faire de deux manières. * La première est la plus évidente : l'instruction de test calcule la condition, le branchement fait ou non le saut dans le programme suivant le résultat de la condition. Le résultat des instructions de test est mémorisé dans des registres de 1 bit, appelés les registres de prédicat. * La seconde méthode procède autrement. Les quatre bits tirés de l'analyse du résultat de la soustraction sont mémorisés dans le registre d'état. Le contenu du registre d'état est ensuite utilisé pour calculer la condition voulue par le branchement. Dans les deux cas, il faut modifier l'organisation précédente pour rajouter les registres et quelques circuits annexes. Il faut notamment ajouter les registres eux-mêmes, mais aussi de quoi gérer leur adressage ou les contrôler. Dans les deux cas, les branchements lisent le contenu de ces registres, et décident alors s'il faut sauter ou non. Dans les deux cas, la soustraction des deux opérandes est réalisée dans le chemin de données, pareil pour la génération des quatre bits intermédiaires. Mais pour le reste, l'organisation change. Le cas le plus simple est clairement celui où on utilise un registre d'état. La seule différence notable avec l'organisation précédente est que l'on ajoute un registre d'état. Mais les autres circuits sont laissés tels quels. La répartition des circuits est aussi modifiée : le calcul des conditions et le multiplexeur sont déplacés dans l'unité de chargement ou dans le séquenceur, alors qu'ils étaient avant dans l'unité de calcul. [[File:Implémentation des branchements avec un registre d'état.png|centre|vignette|upright=2|Implémentation des branchements avec un registre d'état]] L'autre cas est celui où les résultats des conditions sont mémorisés dans des registres à prédicats, connectés au séquenceur. Cela amène deux problèmes : l'instruction de test doit enregistrer le résultat dans le bon registre à prédicat, et il faut aussi lire le bon registre à prédicat suivant le branchement. Il faut donc gérer la sélection en lecture et en écriture. Rappelons que les registres à prédicats sont numérotés, ils ont un nom de registre dédié qui est fourni par le séquenceur. La sélection en lecture et écriture des registres à prédicat se base donc sur ces noms de registre. Pour la sélection en lecture, le choix du registre à prédicat voulu est réalisé par un multiplexeur, commandé par le séquenceur. Le multiplexeur est intégré à l'unité de chargement ou au séquenceur, peu importe. Pour l'enregistrement dans le bon registre à prédicat, le choix est réalisé en sortie de l'unité de calcul, généralement par un démultiplexeur. [[File:Implémentation de l'unité de chargement avec plusieurs registres à prédicats.png|centre|vignette|upright=2|Implémentation de l'unité de chargement avec plusieurs registres à prédicats]] ==L'implémentation des ''skip instructions''== Passons maintenant au cas des ''skip instruction'', qui permettent d'émuler les branchements conditionnels par une instruction de test spéciale. Pour rappel, une ''skip instruction'' permet de zapper l'instruction suivante si la condition testée est fausse, suivie par un branchement inconditionnel. . Dans ce cas, le ''program counter'' est incrémenté normalement si la condition n'est pas respectée, mais il est incrémenté deux fois si elle l'est. Les branchements inconditionnels s’exécutent normalement. Là encore, suivant la condition testée, on trouve un multiplexeur pour choisir le bon résultat de condition. [[File:Implémentation des branchements pseudo-conditionnels dans le séquenceur.png|centre|vignette|upright=1.5|Implémentation des branchements pseudo-conditionnels dans le séquenceur. La gestion de l'adresse de destination de branchement n'est pas illustrée ici, de même que le multiplexeur pour choisir la bonne condition.]] ==L'implémentation des instructions à prédicats== Les instructions à prédicats sont des instructions qui s’exécutent seulement si une condition précise est remplie. Elles sont précédées d'une instruction de test qui met à jour le registre d'état ou un registre à prédicat. L'instruction à prédicat récupère alors le résultat de la condition, calculé par l'instruction de test précédente, et l'utilise pour savoir si elle doit se comporter comme un NOP ou si elle doit faire une opération. Leur implémentation est variable et deux grandes méthodes sont possibles. La première n’exécute pas l'instruction si la condition est invalide, l'autre l’exécute en avance mais n'enregistre pas son résultat dans les registres si la condition se révèle ultérieurement invalide. La première méthode exécute l'opération, mais l'annule si la condition n'est pas respectée. Le calcul des conditions est fait en parallèle de l'autre opération et l'annulation se fait simplement en n'enregistrant pas le résultat de l’opération dans les registres. Le calcul de la condition s'effectue dans le séquenceur, mais le résultat est envoyé dans le chemin de données pour configurer un circuit qui autorise ou non l'enregistrement du résultat dans les registres. Un défaut de cette technique est que l'instruction est effectivement exécutée, ce qui fait que le processeur a consommé un peu d'énergie et a pris un peu de temps pour faire le calcul. L'autre conséquence est que l'instruction mobilise une unité de calcul ou de transfert entre registre, le banc de registres, etc. En soi, ce n'est pas un problème. Mais ça l'est sur les processeurs modernes, qui sont capables d’exécuter plusieurs instructions en parallèle, dans un ordre différent de celui imposé par le programmeur. Nous verrons ces techniques d’exécution en parallèle dans les derniers chapitres du cours. Toujours est-il que sur ces processeurs, une instruction à prédicats va mobiliser des ressources matérielles comme l'ALU ou le bus interne, pour éventuellement fournir un résultat inutile, alors qu'une autre instruction aura pu prendre sa place et calculer des données utiles. [[File:Implémentation des instructions à prédicats.png|centre|vignette|upright=2|Implémentation des instructions à prédicats]] La seconde méthode est la plus intuitive : elle consiste à lire le registre d'état/de prédicat, pour décider s'il faut faire ou non l'opération. Pour cela, le séquenceur lit le registre d'état/à prédicat, et génère les signaux de commande adaptés : il génère les signaux de commande d'un NOP si la condition n'est pas respectée, et il génère les signaux de commande pour l'opération voulue sinon. L’avantage de cette méthode est que l'instruction ne s’exécute pas si la condition n'est pas remplie. Le processeur ne gâche pas d'énergie pour rien, il peut immédiatement passer à l'instruction suivante si celle-ci est disponible, etc. De plus, sur les processeurs modernes capables d’exécuter plusieurs instructions en parallèle, on ne mobilise pas de ressources matérielles si la condition n'est pas remplie et celles-ci sont disponibles pour d'autres instructions. [[File:Implémentation des instructions à prédicats simples.png|centre|vignette|upright=2|Implémentation des instructions à prédicats simples]] <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les méthodes de synchronisation entre processeur et périphériques | prevText=Les méthodes de synchronisation entre processeur et périphériques | next=Les architectures à accumulateur | nextText=Les architectures à accumulateur }} </noinclude> ohsq0wldetubu1diqih6zccqinumhni Wikilivres:GUS2Wiki 4 78643 763868 763296 2026-04-17T11:55:43Z Alexis Jazz 81580 Updating gadget usage statistics from [[Special:GadgetUsage]] ([[phab:T121049]]) 763868 wikitext text/x-wiki {{#ifexist:Project:GUS2Wiki/top|{{/top}}|This page provides a historical record of [[Special:GadgetUsage]] through its page history. To get the data in CSV format, see wikitext. To customize this message or add categories, create [[/top]].}} Les données suivantes sont en cache et ont été mises à jour pour la dernière fois le 2026-04-16T11:44:26Z. {{PLURAL:5000|1=Un seul résultat|5000 résultats}} au maximum {{PLURAL:5000|est disponible|sont disponibles}} dans le cache. {| class="sortable wikitable" ! Gadget !! data-sort-type="number" | Nombre d’utilisateurs !! data-sort-type="number" | Utilisateurs actifs |- |AncreTitres || 33 || 0 |- |ArchiveLinks || 12 || 0 |- |Barre de luxe || 35 || 2 |- |BoutonsLiens || 41 || 0 |- |CategoryAboveAll || 19 || 0 |- |CategorySeparator || 22 || 0 |- |CoinsArrondis || 99 || 1 |- |CollapseSidebox || 35 || 1 |- |CouleurContributions || 33 || 1 |- |CouleursLiens || 25 || 1 |- |DeluxeAdmin || 5 || 1 |- |DeluxeEdit || 36 || 1 |- |DeluxeHistory || 46 || 2 |- |DeluxeImport || 19 || 1 |- |DeluxeRename || 16 || 1 |- |DeluxeSummary || 36 || 2 |- |DevTools || 17 || 1 |- |DirectPageLink || 25 || 1 |- |Emoticons || 43 || 1 |- |EmoticonsToolbar || 45 || 1 |- |FastRevert || 36 || 2 |- |FixArrayAltLines || 22 || 1 |- |FlecheHaut || 66 || 1 |- |GoogleTrans || 28 || 0 |- |HotCats || 81 || 4 |- |JournalDebug || 21 || 1 |- |JournalEnTable || 2 || 1 |- |LeftPaneSwitch || 5 || 1 |- |ListeABordure || 30 || 1 |- |LiveRC || 30 || 0 |- |LocalLiveClock || 27 || 1 |- |Logo || 42 || 0 |- |MobileView || 18 || 2 |- |NavigAdmin || 38 || 0 |- |OngletEditCount || 44 || 2 |- |OngletEditZeroth || 66 || 1 |- |OngletGoogle || 28 || 1 |- |OngletPurge || 54 || 2 |- |OptimizedSuivi || 18 || 0 |- |Popups || 61 || 0 |- |RenommageCategorie || 6 || 2 |- |RestaurationDeluxe || 12 || 1 |- |RevertDiff || 35 || 4 |- |ScriptAutoVersion || 16 || 1 |- |ScriptSidebox || 31 || 0 |- |ScriptToolbar || 42 || 0 |- |SisterProjects || 16 || 1 |- |SkinPreview || 4 || 1 |- |Smart patrol || 2 || 1 |- |SourceLanguage || 24 || 1 |- |SousPages || 57 || 3 |- |SpaceToolbar || 29 || 0 |- |TableUnicode || 55 || 0 |- |Tableau || 66 || 1 |- |TitreDeluxe || 56 || 1 |- |TitreHierarchique || 17 || 0 |- |UTCLiveClock || 26 || 0 |- |UnicodeEditRendering || 29 || 1 |- |WikEd || 29 || 0 |- |autonum || 2 || 0 |- |massblock || 3 || 1 |- |monBrouillon || 19 || 2 |- |perpagecustomization || 16 || 1 |- |recentchangesbox || 15 || 0 |- |searchFocus || 18 || 0 |- |searchbox || 30 || 2 |} * [[Spécial:GadgetUsage]] * [[m:Meta:GUS2Wiki/Script|GUS2Wiki]] <!-- data in CSV format: AncreTitres,33,0 ArchiveLinks,12,0 Barre de luxe,35,2 BoutonsLiens,41,0 CategoryAboveAll,19,0 CategorySeparator,22,0 CoinsArrondis,99,1 CollapseSidebox,35,1 CouleurContributions,33,1 CouleursLiens,25,1 DeluxeAdmin,5,1 DeluxeEdit,36,1 DeluxeHistory,46,2 DeluxeImport,19,1 DeluxeRename,16,1 DeluxeSummary,36,2 DevTools,17,1 DirectPageLink,25,1 Emoticons,43,1 EmoticonsToolbar,45,1 FastRevert,36,2 FixArrayAltLines,22,1 FlecheHaut,66,1 GoogleTrans,28,0 HotCats,81,4 JournalDebug,21,1 JournalEnTable,2,1 LeftPaneSwitch,5,1 ListeABordure,30,1 LiveRC,30,0 LocalLiveClock,27,1 Logo,42,0 MobileView,18,2 NavigAdmin,38,0 OngletEditCount,44,2 OngletEditZeroth,66,1 OngletGoogle,28,1 OngletPurge,54,2 OptimizedSuivi,18,0 Popups,61,0 RenommageCategorie,6,2 RestaurationDeluxe,12,1 RevertDiff,35,4 ScriptAutoVersion,16,1 ScriptSidebox,31,0 ScriptToolbar,42,0 SisterProjects,16,1 SkinPreview,4,1 Smart patrol,2,1 SourceLanguage,24,1 SousPages,57,3 SpaceToolbar,29,0 TableUnicode,55,0 Tableau,66,1 TitreDeluxe,56,1 TitreHierarchique,17,0 UTCLiveClock,26,0 UnicodeEditRendering,29,1 WikEd,29,0 autonum,2,0 massblock,3,1 monBrouillon,19,2 perpagecustomization,16,1 recentchangesbox,15,0 searchFocus,18,0 searchbox,30,2 --> kqh6gbhsjc9aafvbhth2qz9l0c1xs43 Le mouvement Wikimédia/Introduction : Wikimédia n'est pas Wikipédia 0 78999 763841 763745 2026-04-17T04:00:39Z Lionel Scheepmans 20012 763841 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358 correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec pour chacun d'entre eux un objectif et un fonctionnement spécifiques. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Tandis que certains projets accèptent la publication de travaux de recherche ou de productions personnelles. Chose interdite au sein de l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe une fondation chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}.</ref>. À titre d'information concernant la taille de ces , ''Wikimedia Deutschland'', l'association nationale la plus remarquable en matière d’effectifs, emploie plus de 170 personnes<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschland e. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref>, tandis que la [[w:Fondation_Wikimédia|Fondation Wikimédia]], qui assure <ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}.</ref>. Toutes ces informations justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Car imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, alors qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a cependant pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. En janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut effectivement créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de cette consultation, 73 représentants d’organisations affiliées et 984 personnes ont envoyé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] à la Fondation qui reprenait ces termes<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve d’une grande sagesse et ont démonter que de nombreuses personnes connaissent le mouvement uniquement au travers de Wikipédia et sans connaître le reste du mouvement. Il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, ne s’est par exemple développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » n’est apparu dans la [[w:fr:Mouvement_Wikimédia|version francophone du projet]] qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025 et au niveau des 358 autres versions linguistiques de l’encyclopédie, [[d:Q3568028|39 d’entre elles]] seulement possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref >{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} qqx8coli9rxv7ssojcke7nydpzzhl7i 763842 763841 2026-04-17T04:05:50Z Lionel Scheepmans 20012 763842 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358 correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec pour chacun d'entre eux un objectif et un fonctionnement spécifiques. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Tandis que certains projets accèptent la publication de travaux de recherche ou de productions personnelles. Chose interdite au sein de l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}.</ref>. Toutes ces informations justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Car imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, alors qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a cependant pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. En janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut effectivement créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de cette consultation, 73 représentants d’organisations affiliées et 984 personnes ont envoyé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] à la Fondation qui reprenait ces termes<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve d’une grande sagesse et ont démonter que de nombreuses personnes connaissent le mouvement uniquement au travers de Wikipédia et sans connaître le reste du mouvement. Il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, ne s’est par exemple développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » n’est apparu dans la [[w:fr:Mouvement_Wikimédia|version francophone du projet]] qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025 et au niveau des 358 autres versions linguistiques de l’encyclopédie, [[d:Q3568028|39 d’entre elles]] seulement possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref >{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} 77le3skqs53s1506icfzetuskn80bou 763843 763842 2026-04-17T04:23:07Z Lionel Scheepmans 20012 763843 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358 correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec pour chacun d'entre eux un objectif et un fonctionnement spécifiques. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Tandis que certains projets accèptent la publication de travaux de recherche ou de productions personnelles. Chose interdite au sein de l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. Car en janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de celui-ci, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci reprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} 613cikly03tvgjhypb1ioddte0ruvkx 763844 763843 2026-04-17T04:26:57Z Lionel Scheepmans 20012 763844 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec pour chacun d'entre eux un objectif et un fonctionnement spécifiques. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent aussi la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. Car en janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de celui-ci, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci reprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} 1fs5ed0ch5wkiqk507jkhtymm543mgv 763845 763844 2026-04-17T04:29:05Z Lionel Scheepmans 20012 763845 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec un objectif et un fonctionnement spécifiques à chacun. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent aussi la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. Car en janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de celui-ci, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci reprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} 6tbj2lfkarxt7f1pn9eou0lnnuszi4j 763846 763845 2026-04-17T04:30:22Z Lionel Scheepmans 20012 763846 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec un objectif et un fonctionnement spécifiques à chacun. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent même la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il ne faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. Car en janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de celui-ci, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci reprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} d2lxv7i55xk5kau71vnj2n41rl6tj5l 763847 763846 2026-04-17T04:32:49Z Lionel Scheepmans 20012 763847 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec un objectif et un fonctionnement spécifiques à chacun. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent même la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. Car en janvier 2020, une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]] fut créée avant de faire l'objet d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de celui-ci, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci reprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} elgm8m3a09hl3c8i25h9nhcwf8h78qz 763848 763847 2026-04-17T04:38:14Z Lionel Scheepmans 20012 763848 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec un objectif et un fonctionnement spécifiques à chacun. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent même la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. En janvier 2020, ces opposant ont ainsi créé une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]], qui fut le siège d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de ce dernier, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci comprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant dans le nombreux commentaires qui ont précédé la lettre adressée à la Fondation que de nombreuses personnes ne connaissent du mouvement Wikimédia que son encyclopédie. Et il est d'ailleur étonnant de constater que cette méconnaissance existe aussi au sein du mouvement. L' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia, par exemple, ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que l'article « Wikimédia Mouvement » dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 d’entre des plus de 300 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} dljhdwcvc1tlr9vr71cox64b46ew4gl 763849 763848 2026-04-17T04:49:58Z Lionel Scheepmans 20012 763849 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}} </noinclude> Depuis le succès initial de Wikipédia, une myriade de projets de partage des connaissances, d’organisations et de groupes de soutien ont émergé pour former ce qu’on appelle aujourd’hui le mouvement Wikimédia. Même si à ce jour, l’encyclopédie libre reste le projet phare du mouvement, il serait regrettable de réduire l’ensemble du mouvement à cet unique projet pédagogique. Malheureusement, il arrive bien trop souvent qu'une seule version linguistique de Wikipédia suffise pour cacher l’étendue de la forêt Wikimédia. En réalité, Wikimédia représente un mouvement social, international et interculturel complexe, au sein duquel Wikipédia n’est qu’une composante parmi d’autres. D’ailleurs, si un mémoire de master, réalisé en quelques mois a permis de réaliser une [[v:recherche:Culture_fr_Wikipédia|ethnographie du projet Wikipédia en français]]<ref>{{Lien web|titre=Culture fr Wikipédia, ethnographie du projet Wikipédia en français|url=https://web.archive.org/web/20250905132014/https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|auteur=Lionel Scheepmans|site=Wikiversité|année=2011}}.</ref>, une [[v:fr:Recherche:Imagine_un_monde|thèse de doctorat]]<ref name=":0">{{Ouvrage|langue=fr|prénom1=Lionel|nom1=Scheepmans|lien auteur1=user:Lionel Scheepmans|titre=Imagine un monde : quand le mouvement Wikimédia nous aide à penser de manière prospective la société globale et numérique de demain|éditeur=UCL - Université Catholique de Louvain|année=2022|date=17/06/2022|lire en ligne=https://dial.uclouvain.be/pr/boreal/object/boreal:264603|consulté le=2024-03-10|nature article=Thèse de doctorat}}</ref> et un recul de dix ans ont été nécessaires pour synthétiser les origines, l’organisation et les dynamiques globales du mouvement Wikimédia. [[Fichier:Wikimedia project stickers.jpg|vignette|<small>Figure 1. Série d’autocollants reprenant les logos des principaux projets collaboratifs actifs au sein du mouvement Wikimédia.</small>]] À la fin de l'année 2025, l’ampleur numérique du mouvement est effectivement impressionnante. Chaque mois, des millions de modifications bénévoles sont effectuées sur plus de 500 millions de pages web<ref>{{Lien web|auteur=Stat.wikimedia.org|titre=Statistiques de Wikimédia|url=https://web.archive.org/web/20251209230902/https://stats.wikimedia.org/#/all-projects}}.</ref>, réparties sur plus d’un millier de sites<ref>{{Lien web|url=https://web.archive.org/web/20251217134333/https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|titre=All Wikimedia Projects by Size|auteur=Wikistats wmcloud}}.</ref>, dont 358, seulement, correspondent aux [[m:List_of_Wikipedias|versions linguistiques de Wikipédia]]<ref>{{Lien web|url=https://web.archive.org/web/20251208041814/https://meta.wikimedia.org/wiki/List_of_Wikipedias|titre=List_of_Wikipedias|auteur=Méta-Wiki}}.</ref>. Ce qui prouve clairement que les activités en ligne portées par l'ensemble du mouvement Wikimédia dépassent largement ce qui se passe au sein au sein de l’encyclopédie. Il existe ainsi 8 autres projets pédagogiques susceptibles d'atteindre un jour l'envergure et la notoriété de Wikipédia, avec un objectif et un fonctionnement spécifiques à chacun. Contrairement à Wikipédia, ceux-ci ne sont pas soumis à une neutralité de point de vue, ni limité à l'usage de sources secondaires reconnues pour rédiger les articles. Certains de ces projets accèptent même la publication de travaux de recherche ou de productions personnelles, alors que cela est interdit dans l'encyclopédie. De manière détaillée : Wikilivres permet la création de livres pédagogiques, Wikiversité rassemble des supports d'enseignement de tous niveaux et des travaux de recherche, Wikinews se dédie au journalisme collaboratif et citoyen, Wikivoyage développe un guide touristique mondial, pendant que le Wiktionnaire apporte des définitions des mots de toutes les langues et dans toutes les langues. Dans tous ces projets l'usage de sources primaires est autorisée pour permettre de produire du nouveau savoirs. Ce qui, encore une fois, est interdit dans Wikipédia, malgré les biais systémiques que cette politique éditoriale engendre, tels que la sur-représentation de certains genres ou de ce certaines cultures au niveau des articles et de leurs contenus. Tout comme ces projets frères, Wikipédia n'est pas non plus un projet complètement autonome des autres projets Wikimédia. L'encyclopédie compte en effet sur le projet Wikimedia Commons pour héberger l'ensemble de sa médiathèque, sur le projet Wikidata pour lui fournit une base de données structurée, sur le projet Wikisource pour procurer une bibliothèque libre d'accès à ses éditeurs et sur le projet Wikiquote quand il s'agit de retrouver des citations d'auteurs. Tout cela sans oublier les dizaines de sites web, qui traitent l'archivage permanent de Wikipédia et des autres projets Wikimédia, dans le but de fournir des analyses précieuses et aussi libres d’accès que les données qu'elles traitent. Enfin, il faut aussi garder à l'esprit que, au-delà de tous ce site web, le mouvement Wikimédia, c'est aussi de nombreuses institutions et organisations affiliées au mouvement et dispersées dans le monde. Plus précisément, le mouvement Wikimédia regroupe la [[w:Fondation_Wikimédia|Fondation Wikimédia]] chargée de la gestion et l’organisation internationales du mouvement, avec près de 650 salariés de nationalités diverses<ref>{{Lien web|langue=|auteur=Wikimedia Foundation|titre=Les personnes derrière notre connaissance|url=https://web.archive.org/web/20250904032058/https://wikimediafoundation.org/fr/who-we-are/people/|site=|date=|consulté le=}}</ref>, 2 [[m:Wikimedia_thematic_organizations/fr|associations thématiques]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia thematic organisations|url=https://web.archive.org/web/20250926235310/https://meta.wikimedia.org/wiki/Wikimedia_thematic_organizations|site=|date=|consulté le=}}.</ref>, 40 [[m:Wikimedia_chapters/fr|associations locales]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia chapters|url=https://web.archive.org/web/20251006092129/https://meta.wikimedia.org/wiki/Wikimedia_chapters}}</ref>, dont ''Wikimedia Deutschland'' qui regroupe plus de 170 emplyés<ref>{{Lien web|langue=|auteur=RocketReach|titre=Wikimedia Deutschlande. V. Information|url=https://web.archive.org/web/20251218034616/https://rocketreach.co/wikimedia-deutschland-e-v-profile_b5caf600f42e140f|site=|date=|consulté le=}}.</ref> et 141 [[metawiki:Wikimedia_user_groups/fr|groupes d’utilisateurs et utilisatrices]]<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Wikimedia user groups|url=https://web.archive.org/web/20251008041452/https://meta.wikimedia.org/wiki/Wikimedia_User_Groups|site=|date=|consulté le=}}</ref>. Toutes ces explications justifient la nécessité de distinguer le mouvement Wikimédia du projet Wikipédia. Imaginons seulement que l’on se limite à citer Paris pour décrire et comprendre un pays aussi vaste que la France. Certes, Paris est une ville mondialement connue et qui compte plus de deux millions d’habitants et un patrimoine culturel impressionnant. Mais est-ce pour autant qu'il faudrait oublier les autres villes, villages et métropoles françaises ? De plus, la France regroupe aussi des départements et des territoires d’outre-mer, pendant qu'elle entretient des relations et des partenariats internationaux qui dépassent de loin ce qui se passe entre Paris et le reste du monde. Ne pas confondre le mouvement Wikimédia avec le projet Wikipédia relève donc du bon sens. En 2019 cependant, la Fondation Wikimédia a envisagé de se renommer en Fondation Wikipédia et de remplacer le terme « Wikimédia » par « Wikipédia », partout où ce terme est utilisé dans la sphère hors ligne du mouvement. Le but était d’acquérir une plus grande visibilité et d’attirer des milliards de personnes, grâce au nom de marque Wikipédia, considéré comme l’un des plus connus au monde<ref>{{Lien web|langue=|auteur=Zack McCune|titre=Leading with Wikipedia: A brand proposal for 2030|url=https://web.archive.org/web/20210117025153/https://wikimediafoundation.org/news/2019/02/26/leading-with-wikipedia-a-brand-proposal-for-2030/|site=Wikimedia Foundation News|éditeur=|date=26 February 2019|consulté le=}}.</ref>. Ce changement n’a toutefois pas été accepté par de nombreuses personnes actives au sein du mouvement Wikimédia. En janvier 2020, ces opposant ont ainsi créé une page web d’[[m:Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|appel à commentaires]], qui fut le siège d’un long débat<ref name="Requests for comment">{{Lien web|langue=|auteur=Méta-Wiki|titre=Requests for comment/Should the Foundation call itself Wikipedia|url=https://web.archive.org/web/20210905054842/https://meta.wikimedia.org/wiki/Requests_for_comment/Should_the_Foundation_call_itself_Wikipedia|consulté le=}}.</ref>. À l’issue de ce dernier, 73 représentants d’organisations affiliées et 984 personnes ont signé une [[m:Community_open_letter_on_renaming/fr|lettre ouverte]] adressée à la Fondation. Celle-ci comprenait le paragraphe suivant<ref>{{Lien web|langue=|auteur=Méta-Wiki|titre=Lettre ouverte de la Communauté sur le changement de nom|url=https://web.archive.org/web/20210122162652/https://meta.wikimedia.org/wiki/Community_open_letter_on_renaming/fr|consulté le=}}.</ref> : <blockquote> Depuis 20 ans, les bénévoles ont bâti la réputation de Wikipédia en tant que ressource indépendante et communautaire. Les projets du mouvement Wikimédia, dont Wikipédia, se développent autour de la décentralisation et du consensus. Il est essentiel d’établir des distinctions claires entre la Fondation Wikimédia, les affiliés et les contributeurs individuels. Tout changement qui affecte cet équilibre exige le consentement éclairé et la collaboration des communautés. Il est donc très préoccupant de voir « Wikipédia » présenté pour le nom de l’organisation et du mouvement malgré le mécontentement général de la communauté. </blockquote> En s’opposant aux idées de la Fondation, ces membres de la communauté Wikimédia ont ainsi fait preuve de sagesse tout en signalant, dans de nombreux commentaires, que beaucoup de personnes ne connaissent le mouvement Wikimédia qu'à travers son encyclopédie. Or, il est étonnant d'observer que l' [[w:en:Wikimedia_movement|article du projet Wikipédia en anglais]] consacré au mouvement Wikimédia ne s’est développé qu’à partir de 2016<ref>{{Lien web|auteur=Wikipedia|titre=Wikimedia mouvement - old revision|url=https://en.wikipedia.org/w/index.php?title=Wikimedia_movement&oldid=716240586|consulté le=}}.</ref>, tandis que le même article dans la [[w:fr:Mouvement_Wikimédia|version francophone de l'encyclopédie]] n’est apparu qu’en 2019<ref>{{Lien web|auteur=Wikipédia|titre=Mouvement Wikimédia - version archivée|url=https://fr.wikipedia.org/w/index.php?title=Mouvement_Wikim%C3%A9dia&oldid=158268859|consulté le=}}.</ref>. Cela alors qu’en octobre 2025, seulement [[wikidata:Q3568028|39 des 358 autres versions linguistiques]] possédaient un article consacré au mouvement Wikimédia<ref>{{Lien web|auteur=Wikidata|titre=Wikimedia Movement|url=https://web.archive.org/web/20251004091239/https://www.wikidata.org/wiki/Q3568028}}.</ref>. Tous ces éléments justifient donc la nécessité d’offrir au monde une meilleure connaissance du mouvement Wikimédia et des nombreux projets et organisations de production et de partage du savoir qui s’y développent. En ce sens, ce livre est une contribution importante aux défis stratégiques que doit relever le mouvement Wikimédia à l’approche de 2030. Car au-delà des [[wmf:Resolution:Next_Steps_for_Brand_Work,_2021|résolutions]] prises par le conseil d’administration de la Fondation pour développer de nouveaux processus participatifs et délibératifs concernant les questions de marque<ref>{{Lien web|auteur=Wikimedia Foundation Wiki|titre=Resolution:Next Steps for Brand Work, 2021|url=https://web.archive.org/web/20211020232912/https://foundation.wikimedia.org/wiki/Resolution:Next_Steps_for_Brand_Work,_2021|date=|consulté le=}}.</ref>, c’est avant tout un travail d’information et de sensibilisation à destination du grand public qui reste à faire. {| class="wikitable"style="margin: auto;" "text-align:center;" |+ |[[Fichier:Qrcode Culture fr Wikipédia.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Culture_fr_Wikip%C3%A9dia|100x100px|centré|sans_cadre]] |[[Fichier:Qrcode Imagine un monde.svg|lien=https://fr.wikiversity.org/wiki/Recherche:Imagine_un_monde|centré|sans_cadre|100x100px]] |[[Fichier:Code qr Wikiscan.svg|lien=http://wikiscan.org|100x100px|centré|sans_cadre]] |[[Fichier:QR code Statistiques Wikimédia.png|lien=https://stats.wikimedia.org|centré|sans_cadre|100x100px]] |[[Fichier:QR-code Wikistats wmcloud.png|lien=https://wikistats.wmcloud.org/wikimedias_html.php?s=users_asc&th=0&lines=2000|centré|sans cadre|100x100px]] |- |<small>Ethnographie fr Wikipédia</small> |<small>Thèse ''Imagine un monde''</small> |{{Centrer|<small>Wikiscan</small>}} |<small>Statistiques de Wikimédia</small> |<small>Wikistats wmcloud</small> |} ‎<includeonly></includeonly> {{AutoCat}} 47cocncl3te3xai2n8ib20m25vq4x4a Le mouvement Wikimédia/La naissance du mouvement Wikimédia 0 79002 763850 756868 2026-04-17T04:55:55Z Lionel Scheepmans 20012 763850 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Il existe dans l'espace web une multitude d’archives permettant de retracer les événements qui ont conduit à la naissance du mouvement Wikimédia. Cette « préhistoire » du mouvement peut notamment être explorée grâce au réseau d’éducation populaire [[w:Framasoft|Framasoft]], dont le site est apparu environ un an avant la création de la version francophone de Wikipédia. On trouve sur cette plateforme une mine d’informations concernant les [[logiciels libres]] et la [[w:Culture_libre|culture libre]]. Deux épisodes majeurs de l’histoire de l’informatique et d’Internet, malheureusement méconnus du grand public. [[Fichier:Wikipedia Michelangelo.JPG|alt=Le tableau La création d'Adam de Michel-Ange retouché par un Wikimédien de telle sorte à faire apparaitre le logo de Wikipédia entre le doigt de Dieu et celui d'Adam|gauche|vignette|<small>Figure 2. [[w:La_Création_d'Adam_(Michel-Ange)|''La création d’Adam'']] de [[w:Michel-Ange|Michel-Ange]] revisitée par un contributeur de Wikipédia.</small>|300x300px]] Grâce à Framasoft et bien d’[[w:Catégorie:Association_ou_organisme_lié_au_logiciel_libre_en_France|autres associations]], il est possible de découvrir l’organisation et les motivations des millions de personnes qui participent au [[w:fr:mouvement du logiciel libre|mouvement du logiciel libre]]. On y apprend que ce mouvement politique et social a été initié en 1983 par [[w: fr: Richard Stallman|Richard Stallman]], un programmeur du Massachusetts Institute of Technology ([[w:Massachusetts_Institute_of_Technology|MIT]]), qui a eu l’idée d’offrir à chacun une alternative à la marchandisation du secteur informatique. Cette philosophie de libre partage, concrétisée par le projet de Stallman, permit l’essor d’une organisation et d’une éthique de travail originale, développée au sein d’une [[w:Sous-culture|sous-culture]] en vogue dans le milieu informatique depuis les années 1950. Celle-ci fut documentée dans de nombreux ouvrages, dont « ''[[w:L'Éthique_hacker|L’éthique hacker]]'' »<ref>{{Ouvrage|prénom1=Pekka|nom1=Himanen|titre=The Hacker Ethic and the Spirit of the Information Age|éditeur=Vintage|date=2001|isbn=978-0-09-942692-9|consulté le=}}.</ref>, un livre remarquable, dans lequel le philosophe finlandais, [[w:Pekka_Himanen|Pekka Himanen]], analyse en détail les origines de la [[w:Hacker_(sous-culture)|culture hacker]]. Un simple extrait de sa quatrième de couverture<ref>{{Ouvrage|prénom1=Pekka|nom1=Himanen|titre=L'éthique hacker et l'esprit de l'ère de l'information|éditeur=Exils|date=2001|isbn=2-912969-29-8|isbn2=978-2-912969-29-3|oclc=51085264|lire en ligne=|consulté le=}}.</ref> permet d’appréhender la manière de penser de ces informaticiens, rejoints par Richard Stallman durant ses études universitaires, avant d’en devenir l’une des figures les plus charismatiques : <blockquote> On considérait jusqu’à présent le « hacker » comme un voyou d’Internet, responsable d’actes de piratage et de vols de numéros de cartes bancaires. Le philosophe Pekka Himanen voit au contraire les hackers comme des citoyens modèles de l’ère de l’information. Il les considère comme les véritables moteurs d’une profonde mutation sociale. Leur éthique, leur rapport au travail, au temps ou à l’argent, sont fondés sur la passion, le plaisir ou le partage. Cette éthique est radicalement opposée à l’[[w:Éthique_protestante_du_travail|éthique protestante]], telle qu’elle est définie par [[w:Max_Weber|Max Weber]], du travail comme devoir, comme valeur en soi, une morale qui domine encore le monde aujourd’hui. </blockquote> Ces premières informations nous aident déjà à comprendre que le mouvement Wikimédia plonge ses racines dans une transition culturelle remplie d’utopies<ref>{{Article|langue=fr|prénom1=Anne|nom1=Bellon|titre=Qu’est devenue l’utopie d’Internet ?|périodique=Revue Projet|volume=371|numéro=4|date=2019-08-27|issn=0033-0884|doi=10.3917/pro.371.0006|lire en ligne=https://web.archive.org/web/20241209072859/https://shs.cairn.info/revue-projet-2019-4-page-6?lang=fr|consulté le=2025-12-21|pages=6–11}}</ref>. Des utopies qui s’opposaient à ce que l’historien et anthropologue [[w:Karl_Polanyi|Karl Polanyi]]<ref>{{Ouvrage|langue=|prénom1=Karl|nom1=Polanyi|prénom2=Fred|nom2=Block|prénom3=Joseph E|nom3=Stiglitz|titre=The great transformation: the political and economic origins of our time|éditeur=Beacon press|date=2001|isbn=978-0-8070-5643-1|oclc=1277370048}}.</ref> désignait, en 1944, comme un [[w:Libéralisme_économique|libéralisme économique]] qui « subordonne les objectifs humains à la logique d’un mécanisme de marché impersonnel »<ref>Texte original avant sa traduction par www.deepl.com/translator : « ''subordinates human purposes to the logic of an impersonal market mechanism ».''</ref>. Étape par étape, voyons à présent comment les choses se sont construites jusqu’à l’apparition de Wikimédia, en commencant par se demander si ce mouvement ne serait pas devenu l’une des dernières utopies de la révolution numérique. {{AutoCat}} 1185p6s0irhbf3okwgglti44n6ncsck Les cartes graphiques/Le rendu d'une scène 3D : concepts de base 0 79234 763783 763650 2026-04-16T17:33:01Z Mewtow 31375 /* La différence entre rastérisation et lancer de rayons */ 763783 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] L'étape de traitement de la géométrie et celle de rastérisation sont souvent réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} snofqr2duyd2o6e7g5blwpuulsjbq87 763784 763783 2026-04-16T17:33:36Z Mewtow 31375 /* La différence entre rastérisation et lancer de rayons */ 763784 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 3d4kffqg7w9xpf0ql34398g749c6htn 763785 763784 2026-04-16T17:34:14Z Mewtow 31375 /* La différence entre rastérisation et lancer de rayons */ 763785 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. Les deux dernières étapes sont parfois fusionnées, car elles sont très liées, mais nous ne ferons pas cela dans ce cours. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== L'étape de rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. L'étape de rastérisation effectue aussi beaucoup de traitements différents, mais l'idée structurante est que rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 688nog6zfyy0kni8u73gssv6vqjqagy 763786 763785 2026-04-16T17:44:26Z Mewtow 31375 /* La rastérisation et les textures */ 763786 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. Les deux dernières étapes sont parfois fusionnées, car elles sont très liées, mais nous ne ferons pas cela dans ce cours. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples et il n'existe pas de carte graphique qui les implémente. Cependant, une amélioration des rendus précédents a été implémentée sur des cartes graphiques assez anciennes. Le rendu en question est appelé le '''rendu par placage de texture direct''', que nous appellerons rendu direct dans ce qui suit. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, mais il est aujourd'hui abandonné. Il n'a servi que de transition entre rendu 2D et rendu 3D. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} n4hs3ow0guiuvmba4gf0kr2oz561u7m 763787 763786 2026-04-16T17:47:54Z Mewtow 31375 /* Le placage de textures direct */ 763787 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. Les deux dernières étapes sont parfois fusionnées, car elles sont très liées, mais nous ne ferons pas cela dans ce cours. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. Le rendu direct a été utilisé sur les anciennes consoles de jeu et sur les anciennes bornes d'arcade, comme transition entre rendu 2D et rendu 3D, mais il est aujourd'hui abandonné. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} a8gpmkzqp2mhcopy20lcbc93tzec631 763788 763787 2026-04-16T17:49:16Z Mewtow 31375 /* Le placage de textures direct */ 763788 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. Les deux dernières étapes sont parfois fusionnées, car elles sont très liées, mais nous ne ferons pas cela dans ce cours. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} fmw83rsxb3hvuf096xb8uzasl6elxgb 763789 763788 2026-04-16T17:54:12Z Mewtow 31375 /* La différence entre rastérisation et lancer de rayons */ 763789 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Les trois étapes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Il existe plusieurs rendus différents et l'étape de rastérisation dépend fortement du rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Mais quoiqu'il en soit, le rendu d'une image est décomposé en une succession d'étapes, chacune ayant un rôle bien précis. Le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''',qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 2nusiudlnt9mf9vman1x4p2b7i5uomd 763790 763789 2026-04-16T17:57:42Z Mewtow 31375 /* La différence entre rastérisation et lancer de rayons */ 763790 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''', qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 8oxlp6pf2lvsetq05szda15kt43gm9x 763805 763790 2026-04-16T19:58:17Z Mewtow 31375 /* Le test alpha */ 763805 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''', qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ===Les effets de brouillard=== Les '''effets de brouillard''' sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. Le ''view frustum'' utilise alors un plan limite, au-delà duquel on ne voit pas les objets. Mais ce plan limite donne une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on effectue un mélange ''alpha'' entre la couleur du pixel et une ''couleur de brouillard''. La différence est que l'on n'utilise pas la transparence pour faire le mélange, mais un '''coefficient de brouillard''', noté <math>\text{fog}(z)</math>. : <math>\text{Couleur finale} = \text{fog}(z) \times \text{Couleur de l'objet transparent} + [ 1 - \text{fog}(z) ] \times \text{Couleur de l'objet opaque}</math> Le coefficient de brouillard dépend de la coordonnée de profondeur, de la distance du pixel par rapport à la caméra. Le brouillard démarre à une distance <math>z_{fog-start}</math>, et masque totalement les objets à partir d'une distance <math>z_{fog-end}</math>. Entre les deux, le coefficient de brouillard dépend de la distance. OpenGL autorise trois formules de calcul suivantes : : <math>\text{fog}(z) = \frac{z_{fog-end} - z}{z_{fog-end} - z_{fog-start}}</math> : <math>\text{fog}(z) = e^{- k \times z}</math> : <math>\text{fog}(z) = e^{- (k \times z)^2}</math> ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} re1hes5g9i4m5r64c67prwyievie8dp 763806 763805 2026-04-16T19:58:39Z Mewtow 31375 /* Les effets de brouillard */ 763806 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''', qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ===Les effets de brouillard=== Les '''effets de brouillard''' sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. Le ''view frustum'' utilise alors un plan limite, au-delà duquel on ne voit pas les objets. Mais ce plan limite donne une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on effectue un mélange ''alpha'' entre la couleur du pixel et une ''couleur de brouillard''. La différence est que l'on n'utilise pas la transparence pour faire le mélange, mais un '''coefficient de brouillard''', noté <math>\text{fog}(z)</math>. : <math>\text{Couleur finale} = \text{fog}(z) \times \text{Couleur de brouillard + [ 1 - \text{fog}(z) ] \times \text{Couleur du pixel}</math> Le coefficient de brouillard dépend de la coordonnée de profondeur, de la distance du pixel par rapport à la caméra. Le brouillard démarre à une distance <math>z_{fog-start}</math>, et masque totalement les objets à partir d'une distance <math>z_{fog-end}</math>. Entre les deux, le coefficient de brouillard dépend de la distance. OpenGL autorise trois formules de calcul suivantes : : <math>\text{fog}(z) = \frac{z_{fog-end} - z}{z_{fog-end} - z_{fog-start}}</math> : <math>\text{fog}(z) = e^{- k \times z}</math> : <math>\text{fog}(z) = e^{- (k \times z)^2}</math> ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 1uyw9x6lbcfm3utnb7238cc7iz4rfbd 763807 763806 2026-04-16T19:58:56Z Mewtow 31375 /* Les effets de brouillard */ 763807 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''', qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Il connait notamment une composante ''alpha'', qui est ajouté aux trois couleurs RGB. En clair, tout fragment contient une quatrième couleur en plus des couleurs RGB, qui indique si le fragment est plus ou moins transparent. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Une solution, utilisée par beaucoup de moteurs 3D, est de rendre séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, mais les objets transparents doivent être triés selon leur distance. Quelques optimisations permettent cependant de passer outre certaines de ces contraintes. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ===Les effets de brouillard=== Les '''effets de brouillard''' sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. Le ''view frustum'' utilise alors un plan limite, au-delà duquel on ne voit pas les objets. Mais ce plan limite donne une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on effectue un mélange ''alpha'' entre la couleur du pixel et une ''couleur de brouillard''. La différence est que l'on n'utilise pas la transparence pour faire le mélange, mais un '''coefficient de brouillard''', noté <math>\text{fog}(z)</math>. : <math>\text{Couleur finale} = \text{fog}(z) \times \text{Couleur de brouillard} + [ 1 - \text{fog}(z) ] \times \text{Couleur du pixel}</math> Le coefficient de brouillard dépend de la coordonnée de profondeur, de la distance du pixel par rapport à la caméra. Le brouillard démarre à une distance <math>z_{fog-start}</math>, et masque totalement les objets à partir d'une distance <math>z_{fog-end}</math>. Entre les deux, le coefficient de brouillard dépend de la distance. OpenGL autorise trois formules de calcul suivantes : : <math>\text{fog}(z) = \frac{z_{fog-end} - z}{z_{fog-end} - z_{fog-start}}</math> : <math>\text{fog}(z) = e^{- k \times z}</math> : <math>\text{fog}(z) = e^{- (k \times z)^2}</math> ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 1anzns0iamq7kqp6kc7bgmzsqilenhr 763840 763807 2026-04-17T01:29:07Z Mewtow 31375 /* Le mélange alpha */ 763840 wikitext text/x-wiki Le premier jeu à utiliser de la "vraie 3D" texturée fut le jeu Quake, premier du nom. Et depuis sa sortie, la grande majorité des jeux vidéo utilisent de la 3D, même s'il existe encore quelques jeux en 2D. Face à la prolifération des jeux vidéo en 3D, les fabricants de cartes graphiques ont inventé les cartes accélératrices 3D, des cartes vidéo capables d'accélérer le rendu en 3D. Dans ce chapitre, nous allons voir comment elles fonctionnent et comment elles ont évolué dans le temps. Pour comprendre comment celles-ci fonctionnent, il faut faire quelques rapides rappels sur les bases du rendu 3D. ==Les bases du rendu 3D== Une '''scène 3D''' est composée d'un espace en trois dimensions, dans laquelle le moteur d’un jeu vidéo place des objets et les fait bouger. Cette scène est, en première approche, un simple parallélogramme. Un des coins de ce parallélogramme sert d’origine à un système de coordonnées : il est à la position (0, 0, 0), et les axes partent de ce point en suivant les arêtes. Les objets seront placés à des coordonnées bien précises dans ce parallélogramme. ===Les objets 3D et leur géométrie=== [[File:Dolphin triangle mesh.png|vignette|Illustration d'un dauphin, représenté avec des triangles.]] Dans la quasi-totalité des jeux vidéo actuels, les objets et la scène 3D sont modélisés par un assemblage de triangles collés les uns aux autres, ce qui porte le nom de '''maillage''', (''mesh'' en anglais). Il a été tenté dans le passé d'utiliser des quadrilatères (rendu dit en ''quad'') ou d'autres polygones, mais les contraintes techniques ont fait que ces solutions n'ont pas été retenues. [[File:CG WIKI.jpg|centre|vignette|upright=2|Exemple de modèle 3D.]] Les modèles 3D sont définis par leurs sommets, aussi appelés '''vertices''' dans le domaine du rendu 3D. Chaque sommet possède trois coordonnées, qui indiquent sa position dans la scène 3D : abscisse, ordonnée, profondeur. Les sommets sont regroupés en triangles, qui sont formés en combinant trois sommets entre eux. Les anciennes cartes graphiques géraient aussi d'autres formes géométriques, comme des points, des lignes, ou des quadrilatères. Les quadrilatères étaient appelés des ''quads'', et ce terme reviendra occasionnellement dans ce cours. De telles formes basiques, gérées nativement, sont appelées des '''primitives'''. La représentation exacte d'un objet est donc une liste plus ou moins structurée de sommets. La liste doit préciser les coordonnées de chaque sommet, ainsi que comment les relier pour former des triangles. Pour cela, l'objet est représenté par une structure qui contient la liste des sommets, mais aussi de quoi savoir quels sont les sommets reliés entre eux par un segment. Nous en dirons plus dans le chapitre sur le rendu de la géométrie. ===La caméra : le point de vue depuis l'écran=== Outre les objets proprement dit, on trouve une '''caméra''', qui représente les yeux du joueur. Cette caméra est définie au minimum par : * une position ; * par la direction du regard (un vecteur). A la caméra, il faut ajouter tout ce qui permet de déterminer le '''champ de vision'''. Le champ de vision contient tout ce qui est visible à l'écran. Et sa forme dépend de la perspective utilisée. Dans le cas le plus courant dans les jeux vidéos en 3D, il correspond à une '''pyramide de vision''' dont la pointe est la caméra, et dont les faces sont délimitées par les bords de l'écran. A l'intérieur de la pyramide, il y a un rectangle qui représente l'écran du joueur, appelé le '''''viewport'''''. [[File:ViewFrustum.jpg|centre|vignette|upright=2|Caméra.]] [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] La majorité des jeux vidéos ajoutent deux plans : * un ''near plane'' en-deça duquel les objets ne sont pas affichés. Il élimine du champ de vision les objets trop proches. * Un ''far plane'', un '''plan limite''' au-delà duquel on ne voit plus les objets. Il élimine les objets trop lointains. Avec ces deux plans, le champ de vision de la caméra est donc un volume en forme de pyramide tronquée, appelé le '''''view frustum'''''. Le tout est parfois appelée, bien que par abus de langage, la pyramide de vision. Avec d'autres perspectives moins utilisées, le ''view frustum'' est un pavé, mais nous n'en parlerons pas plus dans le cadre de ce cours car elles ne sont presque pas utilisés dans les jeux vidéos actuels. ===Les textures=== Tout objet à rendre en 3D est donc composé d'un assemblage de triangles, et ceux-ci sont éclairés et coloriés par divers algorithmes. Pour rajouter de la couleur, les objets sont recouverts par des '''textures''', des images qui servent de papier peint à un objet. Un objet géométrique est donc recouvert par une ou plusieurs textures qui permettent de le colorier ou de lui appliquer du relief. [[File:Texture+Mapping.jpg|centre|vignette|upright=2|Texture Mapping]] Notons que les textures sont des images comme les autres, codées pixel par pixel. Pour faire la différence entre les pixels de l'écran et les pixels d'une texture, on appelle ces derniers des '''texels'''. Ce terme est assez important, aussi profitez-en pour le mémoriser, nous le réutiliserons dans quelques chapitres. Un autre point lié au fait que les textures sont des images est leur compression, leur format. N'allez pas croire que les textures sont stockées dans un fichier .jpg, .png ou tout autre format de ce genre. Les textures utilisent des formats spécialisés, comme le DXTC1, le S3TC ou d'autres, plus adaptés à leur rôle de texture. Mais qu'il s'agisse d'images normales (.jpg, .png ou autres) ou de textures, toutes sont compressées. Les textures sont compressées pour prendre moins de mémoire. Songez que la compression de texture est terriblement efficace, souvent capable de diviser par 6 la mémoire occupée par une texture. S'en est au point où les textures restent compressées sur le disque dur, mais aussi dans la mémoire vidéo ! Nous en reparlerons dans le chapitre sur la mémoire d'une carte graphique. Plaquer une texture sur un objet peut se faire de deux manières, qui portent les noms de placage de texture inverse et direct. Le placage de texture direct a été utilisé au tout début de la 3D, sur des bornes d'arcade et les consoles de jeu 3DO, PS1, Sega Saturn. De nos jours, on utilise uniquement la technique de placage de texture inverse. Les deux seront décrites dans le détail plus bas. ===La différence entre rastérisation et lancer de rayons=== [[File:Render Types.png|vignette|Même géométrie, plusieurs rendus différents.]] Les techniques de rendu 3D sont nombreuses, mais on peut les classer en deux grands types : le ''lancer de rayons'' et la ''rasterization''. Sans décrire les deux techniques, sachez cependant que le lancer de rayon n'est pas beaucoup utilisé pour les jeux vidéo. Il est surtout utilisé dans la production de films d'animation, d'effets spéciaux, ou d'autres rendu spéciaux. Dans les jeux vidéos, il est surtout utilisé pour quelques effets graphiques, la rasterization restant le mode de rendu principal. La raison principale est que le lancer de rayons demande beaucoup de puissance de calcul. Une autre raison est que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Il a existé des cartes accélératrices permettant d'accélérer le rendu en lancer de rayons, mais elles sont restées confidentielles. Les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, mais ils restent d'un usage marginal et servent de compléments au rendu par rastérization. Un chapitre entier sera dédié aux cartes accélératrices de lancer de rayons et nous verrons pourquoi le lancer de rayons est difficile à implémenter avec des performances convenables, ce qui explique que les jeux vidéo utilisent la ''rasterization''. La rastérisation est structurée autour de trois étapes principales : * Une étape purement logicielle, effectuée par le processeur, où le moteur physique calcule la géométrie de la scène 3D. * Une étape de '''traitement de la géométrie''', qui gère tout ce qui a trait aux sommets et triangles. * Une étape de '''rastérisation''' qui détermine sur quels pixels de l'écran est affiché le triangle. * Une étape de '''traitement des pixels''', qui colorie les pixels et gère les textures. [[File:Graphics pipeline 2 en.svg|centre|vignette|upright=2.5|Pipeline graphique basique.]] Il existe plusieurs rendus différents et la rastérisation ne se fait pas de la même manière selon le rendu utilisé. Il existe des rendus sans textures, d'autres avec, d'autres avec éclairage, d'autres sans, etc. Par contre, l'étape de calcul de la géométrie est la même quel que soit le rendu ! Mieux : le calcul de la géométrie se fait de la même manière entre rastérisation et lancer de rayons, il est le même quelle que soit la technique de rendu 3D utilisée. Les trois étapes précédentes sont réalisées dans des circuits ou processeurs séparés, comme on le verra plus tard. Et cela permet d'utiliser la technique dite du '''pipeline'''. Concrètement, supposons que la carte graphique traite les données par paquets de triangles (en réalité, c'est des paquets de sommets, mais passons). L'étape de traitement de la géométrie peut travailler sur un paquet de triangle, pendant que le paquet précédent est dans l'étape de rastérisation, et que le paquet encore précédent est en train de traiter ses pixels. Cela permet de traiter trois paquets de triangles en même temps, mais à des états d'avancements différents. Mieux que cela : le traitement de la géométrie est lui-même composé d'une succession de sous-étapes, la rasterisation est elle-même découpée en plusieurs sous-étapes, et ainsi de suite. Le nombre d'étapes pour une carte graphique moderne dépasse la dizaine. La rastérisation calcule un rendu 3D avec une suite d'étapes consécutives qui doivent s'enchainer dans un ordre bien précis. L'ensemble de ces étapes est appelé le '''pipeline graphique''', qui sera détaillé dans ce qui suit. ==Le calcul de la géométrie== Le calcul de la géométrie regroupe plusieurs manipulations différentes. La principale demande juste de placer les modèles 3D dans la scène, de placer les objets dans le monde. Puis, il faut centrer la scène 3D sur la caméra. Les deux changements ont pour point commun de demander des changements de repères. Par changement de repères, on veut dire que l'on passe d'un système de coordonnées à un autre. En tout, il existe trois changements de repères distincts qui sont regroupés dans l''''étape de transformation''' : un premier qui place chaque objet 3D dans la scène 3D, un autre qui centre la scène du point de vue de la caméra, et un autre qui corrige la perspective. ===Les trois étapes de transformation=== La première étape place les objets 3D dans la scène 3D. Un modèle 3D est représentée par un ensemble de sommets, qui sont reliés pour former sa surface. Les données du modèle 3D indiquent, pour chaque sommet, sa position par rapport au centre de l'objet qui a les coordonnées (0, 0, 0). La première étape place l'objet 3D à une position dans la scène 3D, déterminée par le moteur physique, qui a des coordonnées (X, Y, Z). Une fois placé dans la scène 3D, le centre de l'objet passe donc des coordonnées (0, 0, 0) aux coordonnées (X, Y, Z) et tous les sommets de l'objet doivent être mis à jour. De plus, l'objet a une certaine orientation : il faut aussi le faire tourner. Enfin, l'objet peut aussi subir une mise à l'échelle : on peut le gonfler ou le faire rapetisser, du moment que cela ne modifie pas sa forme, mais simplement sa taille. En clair, le modèle 3D subit une translation, une rotation et une mise à l'échelle, les trois impliquant une modification des coordonnées des sommets.. [[File:Similarity and congruence transformations.svg|centre|vignette|upright=1.5|Transformations géométriques possibles pour chaque triangle.]] Une fois le placement des différents objets effectué, la carte graphique effectue un changement de coordonnées pour centrer le monde sur la caméra. Au lieu de considérer un des bords de la scène 3D comme étant le point de coordonnées (0, 0, 0), il va passer dans le référentiel de la caméra. Après cette transformation, le point de coordonnées (0, 0, 0) sera la caméra. La direction de la vue du joueur sera alignée avec l'axe de la profondeur (l'axe Z). [[File:View transform.svg|centre|vignette|upright=2|Étape de transformation dans un environnement en deux dimensions : avant et après. On voit que l'on centre le monde sur la position de la caméra et dans sa direction.]] Enfin, il faut aussi corriger la perspective, ce qui est le fait de l'étape de projection, qui modifie la forme du ''view frustum'' sans en modifier le contenu. Différents types de perspective existent et celles-ci ont un impact différent les unes des autres sur le ''view frustum''. Dans le cas qui nous intéresse, le ''view frustum'' passe d’une forme de trapèze tridimensionnel à une forme de pavé dont l'écran est une des faces. ===Les changements de coordonnées se font via des multiplications de matrices=== Les trois étapes précédentes demande de faire des changements de coordonnées, chaque sommet voyant ses coordonnées remplacées par de nouvelles. Or, un changement de coordonnée s'effectue assez simplement, avec des matrices, à savoir des tableaux organisés en lignes et en colonnes avec un nombre dans chaque case. Un changement de coordonnées se fait simplement en multipliant le vecteur (X, Y, Z) des coordonnées d'un sommet par une matrice adéquate. Il existe des matrices pour la translation, la mise à l'échelle, d'autres pour la rotation, une autre pour la transformation de la caméra, une autre pour l'étape de projection, etc. Un changement de coordonnée s'effectue assez simplement en multipliant le vecteur-coordonnées (X, Y, Z) d'un sommet par une matrice adéquate. Un petit problème est que les matrices qui le permettent sont des matrices avec 4 lignes et 4 colonnes. Or, la multiplication demande que le nombre de coordonnées du vecteur soit égal au nombre de colonnes. Pour résoudre ce petit problème, on ajoute une 4éme coordonnée aux sommets, la coordonnée homogène, qui ne sert à rien, et est souvent mise à 1, par défaut. Mais oublions ce détail. Il se trouve que multiplier des matrices amène certaines simplifications. Au lieu de faire plusieurs multiplications de matrices, il est possible de fusionner les matrices en une seule, ce qui permet de simplifier les calculs. Ce qui fait que le placement des objets, changement de repère pour centrer la caméra, et d'autres traitements forts différents sont regroupés ensemble. Le traitement de la géométrie implique, sans surprise, des calculs de géométrie dans l'espace. Et cela implique des opérations mathématiques aux noms barbares : produits scalaires, produits vectoriels, et autres calculs impliquant des vecteurs et/ou des matrices. Et les calculs vectoriels/matriciels impliquent beaucoup d'additions, de soustractions, de multiplications, de division, mais aussi des opérations plus complexes : calculs trigonométriques, racines carrées, inverse d'une racine carrée, etc. Au final, un simple processeur peut faire ce genre de calculs, si on lui fournit le programme adéquat, l'implémentation est assez aisée. Mais on peut aussi implémenter le tout avec un circuit spécialisé, non-programmable. Les deux solutions sont possibles, tant que le circuit dispose d'assez de puissance de calcul. Les cartes graphiques anciennes contenaient un ou plusieurs circuits de multiplication de matrices spécialisés dans l'étape de transformation. Chacun de ces circuits prend un sommet et renvoie le sommet transformé. Ils sont composés d'un gros paquet de multiplieurs et d'additionneurs flottants. Pour plus d'efficacité, les cartes graphiques comportent plusieurs de ces circuits, afin de pouvoir traiter plusieurs sommets en même temps. ==L'élimination des surfaces cachées== Un point important du rendu 3D est que ce que certaines portions de la scène 3D ne sont pas visibles depuis la caméra. Et idéalement, les portions de la scène 3D qui ne sont pas visibles à l'écran ne doivent pas être calculées. A quoi bon calculer des choses qui ne seront pas affichées ? Ce serait gâcher de la puissance de calcul. Et pour cela, de nombreuses optimisations visent à éliminer les calculs inutiles. Elles sont regroupées sous les termes de '''''clipping''''' ou de '''''culling'''''. La différence entre ''culling'' et ''clipping'' n'est pas fixée et la terminologie n'est pas claire. Dans ce qui va suivre, nous n'utiliserons que le terme ''culling''. Les cartes graphiques modernes embarquent diverses méthodes de ''culling'' pour abandonner les calculs quand elles s’aperçoivent que ceux-ci portent sur une partie non-affichée de l'image. Cela fait des économies de puissance de calcul assez appréciables et un gain en performance assez important. Précisons que le ''culling'' peut être plus ou moins précoce suivant le type de rendu 3D utilisé, mais nous verrons cela dans la suite du chapitre. ===Les différentes formes de ''culling''/''clipping''=== La première forme de ''culling'' est le '''''view frustum culling''''', dont le nom indique qu'il s'agit de l'élimination de tout ce qui est situé en-dehors du ''view frustum''. Ce qui est en-dehors du champ de vision de la caméra n'est pas affiché à l'écran n'est pas calculé ou rendu, dans une certaine mesure. Le ''view frustum culling'' est assez trivial : il suffit d'éliminer ce qui n'est pas dans le ''view frustum'' avec quelques calculs de coordonnées assez simples. Quelques subtilités surviennent quand un triangle est partiellement dans le ''view frustrum'', ce qui arrive parfois si le triangle est sur un bord de l'écran. Mais rien d'insurmontable. [[File:View frustum culling.svg|centre|vignette|upright=1|''View frustum culling'' : les parties potentiellement visibles sont en vert, celles invisibles en rouge et celles partiellement visibles en bleu.]] Les autres formes de ''culling'' visent à éliminer ce qui est dans le ''view frustum'', mais qui n'est pas visible depuis la caméra. Pensez à des objets cachés par un autre objet plus proche, par exemple. Ou encore, pensez aux faces à l'arrière d'un objet opaque qui sont cachées par l'avant. Ces deux cas correspondent à deux types de ''culling''. L'élimination des objets masqués par d'autres est appelé l'''occlusion culling''. L'élimination des parties arrières d'un objet est appelé le ''back-face culling''. Dans les deux cas, nous parlerons d''''élimination des surfaces cachées'''. [[File:Occlusion culling example PL.svg|centre|vignette|''Occlusion culling'' : les objets en bleu sont visibles, ceux en rouge sont masqués par les objets en bleu.]] Le lancer de rayons n'a pas besoin d'éliminer les surfaces cachées, il ne calcule que les surfaces visibles. Par contre, la rastérisation demande d'éliminer les surfaces cachées. Sans cela, le rendu est incorrect dans le pire des cas, ou alors le rendu calcule des surfaces invisibles pour rien. Il existe de nombreux algorithmes logiciels pour implémenter l'élimination des surfaces cachées, mais la carte graphique peut aussi s'en charger. L'''occlusion culling'' demande de connaitre la distance à la caméra de chaque triangle. La distance à la caméra est appelée la '''profondeur''' du triangle. Elle est déterminée à l'étape de rastérisation et est calculée à chaque sommet. Lors de la rastérisation, chaque sommet se voit attribuer trois coordonnées : deux coordonnées x et y qui indiquent sa position à l'écran, et une coordonnée de profondeur notée z. ===L'algorithme du peintre=== Pour éliminer les surfaces cachées, la solution la plus simple consiste simplement à rendre les triangles du plus lointain au plus proche. L'idée est que si deux triangles se recouvrent totalement ou partiellement, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Il ne s'agit ni plus ni moins que de l''''algorithme du peintre'''. [[File:Polygons cross.svg|vignette|Polygons cross]] [[File:Painters problem.svg|vignette|Painters problem]] Un problème est que la solution ne marche pas avec certaines configurations particulières, dans le cas où des polygones un peu complexes se chevauchent plusieurs fois. Il se présente rarement dans un rendu 3D normal, mais c'est quand même un cas qu'il faut gérer. Le problème est suffisant pour que cette solution ne soit plus utilisée dans le rendu 3D normal. Un autre problème est que l'algorithme demande de trier les triangles d'une scène 3D selon leur profondeur, du plus profond au moins profond. Et les cartes graphiques n'aiment pas ça, que ce soit les anciennes cartes graphiques comme les modernes. Il s'agit généralement d'une tâche qui est réalisée par le processeur, le CPU, qui est plus efficace que le GPU pour trier des trucs. Aussi, l'algorithme du peintre était utilisé sur d'anciennes cartes graphiques, qui ne géraient pas la géométrie mais seulement les textures et quelques effets de post-processing. Avec ces GPU, les jeux vidéo calculaient la géométrie et la triait sur le CPU, puis effectuaient le reste de la rastérisation sur le GPU. Les anciens jeux en 2.5D comme DOOM ou les DOOM-like, utilisaient une amélioration de l'algorithme du peintre. L'amélioration variait suivant le moteur de jeu utilisé, et donnait soit une technique dite de ''portal rendering'', soit un système de ''Binary Space Partionning'', assez complexes et difficiles à expliquer. Mais il ne s'agissait pas de jeux en 3D, les maps de ces jeux avaient des contraintes qui rendaient cette technique utilisable. Ils n'avaient pas de polygones qui se chevauchent, notamment. ===Le tampon de profondeur=== [[File:Z-buffer no text.jpg|vignette|Z-buffer correspondant à un rendu]] Une autre solution utilise ce qu'on appelle un '''tampon de profondeur''', aussi appelé un ''z-buffer''. Il s'agit d'un tableau, stocké en mémoire vidéo, qui mémorise la coordonnée z de l'objet le plus proche pour chaque pixel. Par défaut, ce tampon de profondeur est initialisé avec la valeur de profondeur maximale, celle du ''far plane'' du ''viewfrustum''. Au fur et à mesure que les objets seront calculés, le tampon de profondeur est mis à jour, conservant ainsi la trace de l'objet le plus proche de la caméra. Si jamais un triangle a une coordonnée z plus grande que celle du tampon de profondeur, cela veut dire qu'il est situé derrière un objet déjà rendu. Il est éliminé (sauf si transparence il y a) et le tampon de profondeur n'a pas à être mis à jour. Dans le cas contraire, l'objet est plus près de la caméra et sa coordonnée z remplace l'ancienne valeur z dans le tampon de profondeur. [[File:Z-buffer.svg|centre|vignette|upright=2.0|Illustration du processus de mise à jour du Z-buffer.]] Il existe des techniques alternatives pour coder la coordonnée de profondeur, qui se distinguent par le fait que la coordonnée z n'est pas proportionnelle à la distance entre le fragment et la caméra. Mais il s'agit là de détails assez mathématiques que je me permets de passer sous silence. Dans la suite de ce cours, nous allons juste parler de profondeur pour regrouper toutes ces techniques, conventionnelles ou alternatives. Toutes les cartes graphiques modernes utilisent un système de ''z-buffer''. C'est la seule solution pour avoir des performances dignes de ce nom. Il faut cependant noter qu'elles utilisent des tampons de profondeur légèrement modifiés, qui ne mémorisent pas la coordonnée de profondeur, mais une valeur dérivée. Pour simplifier, ils ne mémorisent pas la coordonnée de profondeur z, mais son inverse 1/z. Les raisons à cela ne peuvent pas encore être expliquées à ce moment du cours, aussi nous allons simplement dire que c'est une histoire de correction de perspective. Les coordonnées z et 1/z sont codées sur quelques bits, allant de 16 bits pour les anciennes cartes graphiques, à 24/32 bits pour les cartes plus récentes. De nos jours, les Z-buffer de 16 bits sont abandonnés et toutes les cartes graphiques utilisent des coordonnées z de 24 à 32 bits. La raison est que les Z-buffer de 16 bits ont une précision insuffisante, ce qui fait que des artefacts peuvent survenir. Si deux objets sont suffisamment proches, le tampon de profondeur n'a pas la précision suffisante pour discriminer les deux objets. Pour lui, les deux objets sont à la même place. Conséquence : il faut bien choisir un des deux objets et ce choix se fait pixel par pixel, ce qui fait des artefacts visuels apparaissent. On parle alors de '''''z-fighting'''''. Voici ce que cela donne : [[File:Z-fighting.png|centre|vignette|Z-fighting]] Un défaut du tampon de profondeur est qu'il ne gère pas correctement les objets transparents. Dès que de la transparence est présente dans une scène 3D, le tampon de profondeur ne peut pas être utilisé. Une solution pour cela est de rendre une scène 3D en deux phases : une pour les objets opaques, une avec les objets transparents. La où on rend les objets opaques utilise le tampon de profondeur, mais il est désactivé lors de la seconde. ==La rastérisation et les textures== Dans cette section, nous allons voir ensemble l'étape de rastérisation et l'étape de traitement des pixels. La rastérisation est difficile à expliquer, surtout que son rôle exact dépend de la technique de rendu utilisée. Pour simplifier, elle projette un rendu en 3D sur un écran en 2D. Une autre explication tout aussi vague est qu'elle s'occupe la traduction des triangles en un affichage pixelisé à l'écran. Elle détermine à quoi ressemble la scène visible sur l'écran. C'est par exemple lors de cette étape que sont appliquées certaines techniques de ''culling'', qui éliminent les portions non-visibles de l'image, ainsi qu'une correction de la perspective et diverses opérations d'interpolation dont nous parlerons dans plusieurs chapitres. La rastérisation et placage de textures sont deux opérations très liées entre elles. Il existe deux manières principales pour lier les textures à la géométrie : la méthode directe et la méthode inverse (''UV Mapping''). Et les deux font que la rastérisation se fait de manière très différente. Précisons cependant que les rendus les plus simples n'utilisent pas de textures du tout. Ils se contentent de colorier les triangles, voire d'un simple rendu en fil de fer basé sur du tracé de lignes. Dans la suite de cette section, nous allons voir les quatre types de rendu principaux : le rendu en fils de fer, le rendu colorié, et deux rendus utilisant des textures. ===Le rendu en fil de fer=== [[File:Obj lineremoval.png|vignette|Rendu en fil de fer d'un objet 3D.]] Le '''rendu 3D en fils de fer''' est illustré ci-contre. Il s'agit d'un rendu assez ancien, utilisé au tout début de la 3D, sur des machines qu'on aurait du mal à appeler ordinateurs. Il se contente de tracer des lignes à l'écran, lignes qui connectent deux sommets, qui ne sont autres que les arêtes de la géométrie de la scène rendue. Le tout était suffisant pour réaliser quelques jeux vidéos rudimentaires. Les tout premiers jeux vidéos utilisaient ce rendu, l'un d'entre eux étant Maze War, le tout premier FPS. {| |[[File:Maze war.jpg|vignette|Maze war]] |[[File:Maze representation using wireframes 2022-01-10.gif|centre|vignette|Maze representation using wireframes 2022-01-10]] |} Le monde est calculé en 3D, il y a toujours un calcul de la géométrie, la scène est rastérisée normalement, les portions invisbles de l'image sont retirées, mais il n'y a pas d'application de textures après rastérisation. A la place, un algorithme de tracé de ligne trace les lignes à l'écran. Quand un triangle passe l'étape de rastérisation, l'étape de rastérisation fournit la position des trois sommets sur l'écran. En clair, elle fournit les coordonnées de trois pixels, un par sommet. A la suite, un algorithme de tracé de ligne trace trois lignes, une par paire de sommet. L'implémentation demande juste d'avoir une unité de calcul géométrique, une unité de rastérisation, et un VDC qui supporte le tracé de lignes. Elle est donc assez simple et ne demande pas de circuits de gestion des textures ni de ROP. Le VDC écrit directement dans le ''framebuffer'' les lignes à tracer. Il a existé des proto-cartes graphiques spécialisées dans ce genre de rendu, comme le '''''Line Drawing System-1''''' de l'entreprise Eans & Sutherland. Nous détaillerons son fonctionnement dans quelques chapitres. ===Le rendu à primitives colorées=== [[File:MiniFighter.png|vignette|upright=1|Exemple de rendu pouvant être obtenu avec des sommets colorés.]] Une amélioration du rendu précédent utilise des triangles/''quads'' coloriés. Chaque triangle ou ''quad'' est associé à une couleur, et cette couleur est dessinée sur le triangle/''quad''après la rastérisation. Le rendu est une amélioration du rendu en fils de fer. L'idée est que chaque triangle/''quad'' est associé à une couleur, qui est dessinée sur le triangle/''quad'' après la rastérisation. La technique est nommée ''colored vertices'' en anglais, nous parlerons de '''rendu à maillage coloré'''. [[File:Malla irregular de triángulos modelizando una superficie convexa.png|centre|vignette|upright=2|Maillage coloré.]] La couleur est propagée lors des calculs géométriques et de la rastérisation, sans subir de modifications. Une fois un rendu en fils de fer effectué, la couleur du triangle est récupérée. Le triangle/''quad'' rendu correspond à un triangle/''quad'' à l'écran. Et l'intérieur de ce triangle/''quad'' est colorié avec la couleur transmise. Pour cela, on utilise encore une fois une fonction du VDC : celle du remplissage de figure géométrique. Nous l’avions vu en parlant des VDC à accélération 2D, mais elle est souvent prise en charge par les ''blitters''. Ils peuvent remplir une figure géométrique avec une couleur unique, on réutilise cette fonction pour colorier le triangle/''quad''. L'étape de rastérisation fournit les coordonnées des sommets de la figure géométrique, le ''blitter'' les utilise pour colorier la figure géométrique. Niveau matériel, quelques bornes d'arcade ont utilisé ce rendu. La toute première borne d'arcade utilisant le rendu à maillage coloré est celle du jeu I Robot, d'Atari, sorti en 1983. Par la suite, dès 1988, les cartes d'arcades Namco System 21 et les bornes d'arcades Sega Model 1 utilisaient ce genre de rendu. On peut s'en rendre compte en regardant les graphismes des jeux tournant sur ces bornes d'arcade. Des jeux comme Virtua Racing, Virtua Fighter ou Virtua Formula sont assez parlants à ce niveau. Leurs graphismes sont assez anguleux et on voit qu'ils sont basés sur des triangles uniformément colorés. Pour ceux qui veulent en savoir plus sur la toute première borne d'arcade en rendu à maillage colorée, la borne ''I Robot'' d'Atari, voici une vidéo youtube à ce sujet : * [https://www.youtube.com/watch?v=6miEkPENsT0 I Robot d'Atari, le pionnier de la 3D Flat.] ===Le placage de textures direct=== Les deux rendus précédents sont très simples, mais n'utilisent pas de textures. Et il est temps de voir les deux rendus qui utilisent des textures. Il y en a deux types, appelés rendu avec placage de texture direct et indirect, nous allons voir le '''rendu par placage de texture direct''' en premier. Et nous l'appellerons ''rendu direct'' dans ce qui suit, pour simplifier les explications. L'idée est assez simple et peut utiliser aussi bien des triangles que des ''quads'', mais nous allons partir du principe qu'elle utilise des '''''quads''''', à savoir que les objets 3D sont composés de quadrilatères. Lorsqu'un ''quad'' est rastérisé, sa forme à l'écran est un rectangle déformé par la perspective. On obtient un rectangle si le ''quad'' est vu de face, un trapèze si on le voit de biais. Et le ''sprite'' doit être déformé de la même manière que le ''quad''. L'idée est que tout quad est associé à une texture, à un sprite. La figure géométrique qui correspond à un ''quad'' à l'écran est remplie non pas par une couleur uniforme, mais par un ''sprite'' rectangulaire. Il suffit techniquement de recopier le ''sprite'' à l'écran, c'est à dire dans la figure géométrique, au bon endroit dans le ''framebuffer''. Le rendu direct est en effet un intermédiaire entre rendu 2D à base de ''sprite'' et rendu 3D moderne. La géométrie est rendue en 3D pour générer des ''quads'', mais ces ''quads'' ne servent à guider la copie des sprites/textures dans le ''framebuffer''. [[File:TextureMapping.png|centre|vignette|upright=2|Exemple caricatural de placage de texture sur un ''quad''.]] La subtilité est que le sprite est déformé de manière à rentrer dans un quadrilatère, qui n'est pas forcément un rectangle à l'écran, mais est déformé par la perspective et son orientation en 3D. Le sprite doit être déformé de deux manières : il doit être agrandi/réduit en fonction de la taille de la figure affichée à l'écran, tourné en fonction de l'orientation du ''quad'', déformé pour gérer la perspective. Pour cela, il faut connaitre les coordonnées de profondeur de chaque bord d'un ''quad'', et de faire quelques calculs. N'importe quel VDC incluant un ''blitter'' avec une gestion du zoom/rotation des sprites peut le faire. : Si on veut avoir de beaux graphismes, il vaut mieux appliquer un filtre pour lisser le sprite envoyé dans le trapèze, filtre qui se résume à une opération d'interpolation et n'est pas très différent du filtrage de texture qui lisse les textures à l'écran. Un autre point est que les ''quads'' doivent être rendus du plus lointain au plus proche. Sans cela, on obtient rapidement des erreurs de rendu. L'idée est que si deux quads se chevauchent, on doit dessiner celui qui est derrière, puis celui qui est devant. Le dessin du second va recouvrir le premier. L'écriture du sprite du second quad écrasera les données du premier quad, pour les portions recouvertes, lors de l'écriture du sprite dans le ''framebuffer''. Quelque chose qui devrait vous rappeler le rendu 2D, où les sprites sont rendus du plus lointain au plus proche. Le rendu inverse utilise très souvent des triangles pour la géométrie, alors que le rendu direct a tendance à utiliser des ''quads'', mais il ne s'agit pas d'une différence stricte. L'usage de triangles/''quads'' peut se faire aussi bien avec un rendu direct comme avec un rendu inverse. Cependant, le rendu en ''quad'' se marie très bien au rendu direct, alors que le rendu en triangle colle mieux au rendu inverse. L'avantage de cette technique est qu'on parcourt les textures dans un ordre bien précis. Par exemple, on peut parcourir la texture ligne par ligne, l'exploiter par blocs de 4*4 pixels, etc. Et accéder à une texture de manière prédictible se marie bien avec l'usage de mémoires caches, ce qui est un avantage en matière de performances. Mais un même pixel du ''framebuffer'' est écrit plusieurs fois quand plusieurs quads se superposent, alors que le rendu inverse gère la situation avec une seule écriture (sauf si usage de la transparence). De plus, la gestion de la transparence était compliquée et les jeux devaient ruser en utilisation des solutions logicielles assez complexes. Niveau implémentation matérielle, une carte graphique en rendu direct demande juste trois circuits. Le premier est un circuit de calcul géométrique, qui rend la scène 3D. Le tri des quads est souvent réalisé par le processeur principal, et non pas par un circuit séparé. Toutes les étapes au-delà de l'étape de rastérisation étaient prises en charge par un VDC amélioré, qui écrivait des sprites/textures directement dans le ''framebuffer''. {|class="wikitable" |- ! Géométrie | Processeurs dédiés programmé pour émuler le pipeline graphique |- ! Tri des quads du plus lointain au plus proche | Processeur principal (implémentation logicielle) |- ! Application des textures | ''Blitter'' amélioré, capable de faire tourner et de zoomer sur des ''sprites''. |} L'implémentation était très simple et réutilisait des composants déjà existants : des VDC 2D pour l'application des textures, des processeurs dédiés pour la géométrie. Les unités de calcul de la géométrie étaient généralement implémentées avec un ou plusieurs processeurs dédiés. Vu qu'on savait déjà effectuer le rendu géométrique en logiciel, pas besoin de créer un circuit sur mesure. Il suffisait de dédier un processeur spécialisé rien que pour les calculs géométriques et on lui faisait exécuter un code déjà bien connu à la base. En clair, ils utilisaient un code spécifique pour émuler un circuit fixe. C'était clairement la solution la plus adaptée pour l'époque. Les unités géométriques étaient des processeurs RISC, normalement utilisés dans l'embarqué ou sur des serveurs. Elles utilisaient parfois des DSP. Pour rappel, les DSP des processeurs de traitement de signal assez communs, pas spécialement dédiés aux rendu 3D, mais spécialisé dans le traitement de signal audio, vidéo et autre. Ils avaient un jeu d'instruction assez proche de celui des cartes graphiques actuelles, et supportaient de nombreuses instructions utiles pour le rendu 3D. [[File:Sega ST-V Dynamite Deka PCB 20100324.jpg|vignette|Sega ST-V Dynamite Deka PCB 20100324]] Le rendu direct a été utilisé dans la période de transition entre rendu 2D et rendu 3D, car il était très adapté pour faire cette transition. Coupler un VDC à un processeur pour la géométrie était particulièrement simple à l'époque. Le rendu direct a été utilisé sur des bornes d'arcade dès les années 90. Outre les bornes d'arcade, quelques consoles de 5ème génération utilisaient le rendu direct, avec les mêmes solutions matérielles. La géométrie était calculée sur plusieurs processeurs dédiés. Le reste du pipeline était géré par un VDC 2D qui implémentait le placage de textures. Deux consoles étaient dans ce cas : la 3DO, et la Sega Saturn. Le rendu direct est aujourd'hui abandonné. ===Le placage de textures inverse=== Le rendu précédent, le rendu direct, permet d'appliquer des textures directement dans le ''framebuffer''. Mais comme dit plus haut, il existe une seconde technique pour plaquer des textures, appelé le '''placage de texture inverse''', aussi appelé l'''UV Mapping''. Elle associe une texture complète pour un modèle 3D,contrairement au placage de tecture direct qui associe une texture par ''quad''/triangle. L'idée est que l'on attribue un texel à chaque sommet. Plus précisémment, chaque sommet est associé à des '''coordonnées de texture''', qui précisent quelle texture appliquer, mais aussi où se situe le texel à appliquer dans la texture. Par exemple, la coordonnée de texture peut dire : je veux le pixel qui est à ligne 5, colonne 27 dans cette texture. La correspondance entre texture et géométrie est réalisée lorsque les créateurs de jeu vidéo conçoivent le modèle de l'objet. [[File:Texture Mapping example.png|centre|vignette|upright=2|Exemple de placage de texture.]] Dans les faits, on n'utilise pas de coordonnées entières de ce type, mais deux nombres flottants compris entre 0 et 1. La coordonnée 0,0 correspond au texel en bas à gauche, celui de coordonnée 1,1 est tout en haut à droite. L'avantage est que ces coordonnées sont indépendantes de la résolution de la texture, ce qui aura des avantages pour certaines techniques de rendu, comme le ''mip-mapping''. Les deux coordonnées de texture sont notées u,v avec DirectX, ou encore s,t dans le cas général : u est la coordonnée horizontale, v la verticale. [[File:UVMapping.png|centre|vignette|upright=2|UV Mapping]] Avec le placage de texture inverse, la rastérisation se fait grosso-modo en trois étapes : la rastérisation proprement dite, le placage de textures, et les opérations finales qui écrivent un pixel dans le ''framebuffer''. Au niveau du matériel, ainsi que dans la plupart des API 3D, les trois étapes sont réalisées par des circuits séparés. [[File:01 3D-Rasterung-a.svg|vignette|Illustration du principe de la rasterization. La surface correspondant à l'écran est subdivisée en pixels carrés, de coordonnées x et y. La caméra est placée au point e. Pour chaque pixel, on trace une droite qui part de la caméra et qui passe par le pixel considéré. L'intersection entre une surface et cette droite se fait en un point, appartenant à un triangle.]] Lors de la rasterisation, chaque triangle se voit attribuer un ou plusieurs pixels à l'écran. Pour bien comprendre, imaginez une ligne droite qui part de caméra et qui passe par un pixel sur le plan de l'écran. Cette ligne intersecte 0, 1 ou plusieurs objets dans la scène 3D. Les triangles situés ces intersections entre cette ligne et les objets rencontrés seront associés au pixel correspondant. L'étape de rastérisation prend en entrée un triangle et renvoie la coordonnée x,y du pixel associé. Il s'agit là d'une simplification, car un triangle tend à occuper plusieurs pixels sur l'écran. L'étape de rastérisation fournit la liste de tous les pixels occupés par un triangle, et les traite un par un. Quand un triangle est rastérisé, le rasteriseur détermine la coordonnée x,y du premier pixel, applique une texture dessus, puis passe au suivant, et rebelote jusqu'à ce que tous les pixels occupés par le triangles aient été traités. L'implémentation matérielle du placage de texture inverse est beaucoup plus complexe que pour les autres techniques. Pour être franc, nous allons passer le reste du cours à parler de l'implémentation matérielle du placage de texture inverse, ce qui prendra plus d'une dizaine de chapitres. ==La transparence, les fragments et les ROPs== Dans ce qui suit, nous allons parler uniquement de la rastérisation avec placage de textures inverse. Les autres formes de rastérisation ne seront pas abordées. La raison est que tous les GPUs modernes utilisent cette forme de rastérisation, les exceptions étant rares. De même, ils utilisent un tampon de profondeur, pour l'élimination des surfaces cachées. La rastérisation effectue donc des calculs géométriques, suivis d'une étape de rastérisation, puis de placage des textures. Ces trois étapes sont réalisées par une unité géométrique, une unité de rastérisation, et un circuit de placage de textures. Du moins sur le principe, car les cartes graphiques modernes ont fortement optimisé l'implémentation et n'ont pas hésité à fusionner certains circuits. Mais nous verrons cela en temps voulu, nous n'allons pas résumer plusieurs décennies d'innovation technologique en quelques paragraphes. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures |} Mais où mettre le tampon de profondeur ? Intuitivement, on se dit qu'il vaut mieux faire l'élimination des surfaces cachées le plus tôt possible, dès que la coordonnée de profondeur est connue. Et elle est connu à l'étape de rastérisation, une fois les sommets transformés. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Tampon de profondeur | Placage de textures |} En réalité, la profondeur des fragments est gérée par un circuit appelé le '''''Raster Operations Pipeline''''' (ROP), situé à la toute fin du pipeline graphique. Dans ce qui suit, nous utiliserons l'abréviation ROP pour simplifier les explications. Le ROP effectue quelques traitements sur les fragments, avant d'enregistrer l'image finale dans la mémoire vidéo. Il est placé à la fin du pipeline pour gérer correctement la transparence. Et nous allons voir pourquoi la transparence est gérée à la fin du pipeline. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} ===Le mélange ''alpha''=== La transparence se manifeste quand plusieurs objets sont l'un derrière l'autre. Histoire de simplifier les explications, nous allons d'abord voir le cas où un objet semi-transparent est devant un objet opaque. La couleur perçue est alors un mélange de la couleur de l'objet opaque et celle de l'objet semi-transparent. Le mélange dépend d'à quel point l'objet semi-transparent est transparent. Avec un objet parfaitement transparent, seul l'objet opaque est visible. Avec un objet à moitié transparent, la couleur finale sera pour moitié celle de l'objet opaque, pour moitié celle de l'objet semi-transparent. Et c'est pareil pour les cas intermédiaires entre un objet totalement transparent et un objet totalement opaque. La transparence d'un objet/pixel est définie par un nombre, appelé la '''composante ''alpha'''''. Plus la composante alpha est élevée, plus le pixel est opaque. Elle vaut 0 pour un objet opaque et 1 pour un objet transparent. Elle est ajoutée aux composantes RGB, ce qui fait que tout fragment contient une "couleur de transparence" en plus des couleurs RGB. Elle agit comme un coefficient qui dit comment mélanger la couleur d'un objet transparent et d'un objet opaque. Le calcul de la transparence est une moyenne pondérée par la composante alpha. On parle alors d''''''alpha blending'''''. : <math>\text{Couleur finale} = \alpha \times \text{Couleur de l'objet transparent} + (1 - \alpha) \times \text{Couleur de l'objet opaque}</math> [[File:Texture splatting.png|centre|vignette|upright=2.0|Calcul de transparence. La première ligne montre le produit pour l'objet transparent, la seconde ligne est celle de l'objet opaque. La troisième ligne est celle de l'addition finale.]] Maintenant, qu'en est-il du cas où plusieurs objets sont superposés ? Si vous tracez une demi-droite dont l'origine est la caméra et qui passe par le pixel, il arrive qu'elle intersecte la géométrie en plusieurs points, un point par objet sur la ligne du regarde. Sans transparence, l'objet le plus proche cache tous les autres et c'est donc lui qui décide de la couleur du pixel. Mais avec un objet transparent, la couleur finale est un mélange de la couleur de plusieurs points d'intersection. Il faut donc calculer un pseudo-pixel pour chaque point d'intersection, auquel on donne le nom de '''fragment'''. Un fragment possède une position à l'écran, une coordonnée de profondeur, une couleur, ainsi que quelques autres informations potentiellement utiles. Les fragments attribués à un même pixel, qui sont à la même position sur l'écran, sont combinés pour obtenir la couleur finale de ce pixel. Il est possible d'utiliser le mélange ''alpha'' pour cela. Il suffit de faire le mélange ''alpha'' entre le fragment qui vient d'être calculé, et le pixel dans le ''framebuffer''. Pour cela, le fragment a une composante ''alpha'', qui est ajouté aux trois couleurs RGB. Le pixel déjà dans le ''framebuffer'' est un résultat temporaire, né du mélange ''alpha'' de tous les fragments précédents. Un défaut de cette méthode est qu'elle fonctionne assez mal avec un tampon de profondeur. Si le tampon de profondeur est activé, le mélange ''alpha'' ne fonctionne que si les objets sont rendus du plus lointain au plus proche. Et procéder dans cet ordre a un défaut : on dessine des objets dans le ''framebuffer'', pour qu'ensuite les objets devant écrasent ce qui a déjà été dessiné. Un même pixel peut donc être dessiné plusieurs fois, dont une seule sera pertinente. Et ces écritures utilisent de la bande passante mémoire, qui est une ressource précieuse sur un GPU moderne. Il s'agit d'un phénomène appelé l'''overdraw'', ou sur-dessinage en français. Quelques optimisations permettent d'éliminer l'''overdraw'' en rendant les objets du plus proche au plus lointain, d'autres permettent de dessiner des objets dans un ordre arbitraire, mais nous ne pouvons pas en parler ici. Beaucoup de moteurs 3D rendent séparément les objets opaques et transparents. Une première passe rend les objets opaques, puis les objets transparents sont rendus dans une seconde passe. Les objets opaques sont rendus dans le désordre, ce qui fait qu'on n'a pas à les trier, alors que les objets transparents doivent être triés selon leur distance. un autre avantage est que le mélange ''alpha'' est désactivé lors de la première passe, alors que c'est la mise à jour du tampon de profondeur qui est désactivé lors de la seconde passe, ce qui augmente un peu les performances dans les deux cas. ===Le test ''alpha''=== Le test ''alpha'' est une technique qui permet d'annuler le rendu d'un fragment en fonction de sa transparence. Si la composante alpha est en-dessous ou au-dessus d'un seuil, le fragment est simplement abandonné. Le seuil en question est configurable, de même que la comparaison utilisée : on peut éliminer le fragment si sa transparence est au-dessus d'un certain seuil, en-dessous, égal, différent, etc. Il s'agit d'une optimisation qui est utile dans certains scénarios spécifiques. Par exemple, si l'objet a une transparence très élevée, du genre 95%, autant le compter comme complétement transparent, afin d'éviter des opérations de mélange ''alpha''. En effet, les opérations de mélange ''alpha'' sont très lentes, car elles demandent de faire des opérations de lecture-écriture en mémoire vidéo : on lit un pixel dans le ''framebuffer'', on applique le mélange ''alpha'' et on écrit le résultat en mémoire vidéo. L'''alpha test'' permet donc de gagner en performance au prix d'une baisse de la qualité d'image. Il y a cependant des cas où l'usage du test ''alpha'' est primordial, au-delà d'une question de performances. Un exemple classique est celui du rendu du feuillage dans un jeu 3D. Un feuillage est composé en assemblant plusieurs images de feuilles. Chaque feuille est un carré sur lequel on place une texture de feuille, qui est opaque pour la partie verte des feuilles, transparente pour le reste. Les carrés ne sont cependant pas superposés, mais s'intersectent fortement, ce qui fait que le mélange ''alpha'' ne donne pas de bons résultats. L'usage du test ''alpha'' permet d'obtenir un rendu correct. Pour d'informations via ce lien : * [https://bgolus.medium.com/anti-aliased-alpha-test-the-esoteric-alpha-to-coverage-8b177335ae4f Anti-aliased Alpha Test: The Esoteric Alpha To Coverage]. ===Les effets de brouillard=== Les '''effets de brouillard''' sont nécessaires dans certains jeux vidéo pour l'ambiance (pensez à des jeux d'horreur comme Silent Hill), mais ils ont surtout été utilisés pour économiser des calculs. L'idée est de ne pas calculer les graphismes au-delà d'une certaine distance, sans que cela se voie. Le ''view frustum'' utilise alors un plan limite, au-delà duquel on ne voit pas les objets. Mais ce plan limite donne une cassure inesthétique dans le rendu. Pour masquer cette cassure, les programmeurs ajoutaient un effet de brouillard. Les objets au-delà du plan limite étaient totalement dans le brouillard, puis ce brouillard se réduisait progressivement en se rapprochant de la caméra, avant de s'annuler à partir d'une certaine distance. Pour calculer le brouillard, on effectue un mélange ''alpha'' entre la couleur du pixel et une ''couleur de brouillard''. La différence est que l'on n'utilise pas la transparence pour faire le mélange, mais un '''coefficient de brouillard''', noté <math>\text{fog}(z)</math>. : <math>\text{Couleur finale} = \text{fog}(z) \times \text{Couleur de brouillard} + [ 1 - \text{fog}(z) ] \times \text{Couleur du pixel}</math> Le coefficient de brouillard dépend de la coordonnée de profondeur, de la distance du pixel par rapport à la caméra. Le brouillard démarre à une distance <math>z_{fog-start}</math>, et masque totalement les objets à partir d'une distance <math>z_{fog-end}</math>. Entre les deux, le coefficient de brouillard dépend de la distance. OpenGL autorise trois formules de calcul suivantes : : <math>\text{fog}(z) = \frac{z_{fog-end} - z}{z_{fog-end} - z_{fog-start}}</math> : <math>\text{fog}(z) = e^{- k \times z}</math> : <math>\text{fog}(z) = e^{- (k \times z)^2}</math> ==L'éclairage d'une scène 3D== L'éclairage d'une scène 3D calcule les ombres, mais aussi la luminosité de chaque pixel, ainsi que bien d'autres effets graphiques. Les algorithmes d'éclairage ont longtemps été implémentés directement en matériel, les cartes graphiques géraient l'éclairage dans des circuits spécialisés. Aussi, il est important de voir ces algorithmes d'éclairage. Il est possible d'implémenter l'éclairage à deux endroits différents du pipeline : juste avant la rastérisation, et après la rastérisation. ===Les sources de lumière et les couleurs associées=== L'éclairage d'une scène 3D provient de sources de lumières, comme des lampes, des torches, le soleil, etc. Il existe de nombreux types de sources de lumière, et nous n'allons parler que des principales. Elles sont au nombre de quatre et elles sont illustrées ci-dessous. [[File:3udUJ.gif|centre|vignette|upright=2|Types de sources de lumière.]] [[File:Graphics lightmodel directional.png|vignette|upright=1.0|Source de lumière directionnelle.]] Les '''sources directionnelles''' servent à modéliser des sources de lumière très éloignées, comme le soleil ou la lune. Elles sont simplement définies par un vecteur qui indique la direction de la lumière, rien de plus. Les '''sources ponctuelles''' sont des points, qui émettent de la lumière dans toutes les directions. Elles sont définies par une position, et une intensité lumineuse, éventuellement la couleur de la lumière émise. Il existe deyux types de sources de lumière ponctuelles. * Le premières émettent de manière égale dans toutes les directions. Elles sont appelées des ''point light'' dans le schéma du dessus. * Les secondes émettent de la lumière dans une '''direction privilégiée'''. L'exemple le plus parlant est celui d'une lampe-torche : elle émet de la lumière "tout droit", dans la direction où la lampe est orientée. Elles sont appelées des ''sport light'' dans le schéma du dessus. La direction privilégiée est un vecteur, notée v dans le schéma du dessous. [[File:Graphics lightmodel ambient.png|vignette|upright=1.0|Lumière ambiante.]] En théorie, la lumière rebondit sur les surfaces et a tendance à se disperser un peu partout à force de rebondir. C'est ce qui explique qu'on arrive à voir à l'intérieur d'une pièce si une fenêtre est ouverte. Il en résulte un certain '''éclairage ambiant''', qui est assez difficile à représenter dans un moteur de rendu 3D. Auparavant, l'éclairage ambiant était simulé par une lumière égale en tout point de la scène 3D, appelée simplement la '''lumière ambiante'''. Précisément, on suppose que la lumière ambiante en un point vient de toutes les directions et a une intensité constante, identique dans toutes les directions. Le tout est illustré ci-contre. C'est assez irréaliste, mais ça donne une bonne approximation de la lumière ambiante. ===La lumière incidente : le terme géométrique=== Pour simplifier, nous allons supposer que l'éclairage est calculé pour chaque sommet, pas par triangle. C'est de loin le cas le plus courant, aussi ce n'est pas une simplification abusive. La lumière qui arrive sur un sommet est appelée la '''lumière incidente'''. La couleur d'un sommet dépend de deux choses : la lumière incidente directe, comment il réfléchit cette lumière. Mathématiquement, il est possible de résumer cela avec le produit de deux termes : l'intensité de la lumière incidente, une fonction qui indique comment la surface réfléchit la lumière incidente. La fonction en question est appelée la '''réflectivité bidirectionnelle'''. Le terme anglais est ''bidirectional reflectance distribution function'', abrévié en BRDF, et nous utiliserons cette abréviation dans ce qui suit. : <math>\text{Couleur finale} = \text{Lumière incidente} \times BRDF(...)</math> La lumière incidente vient soit directement des sources de lumière, soit de la lumière qui a rebondit sur d'autres objets proches. La première est appelée la lumière directe, celle qui vient des rebonds s'appelle la lumière indirecte. Pour simplifier, la lumière indirecte est gérée par la lumière ambiante, nous passons sous silence les techniques d'illumination globale. En clair : nous allons nous limiter au cas où la lumière incidente vient directement d'une source de lumière, pas d'un rebond. Intuitivement, la lumière incidente est simplement égale à l'intensité de la source de lumière. Sauf que ce n'est qu'une approximation, et une assez mauvaise. En réalité, l'approximation est bonne si la lumière arrive proche de la verticale, mais elle est d'autant plus mauvaise que la lumière arrive penchée, voire rasante. La raison : la lumière incidente sera étalée sur une surface plus grande, si elle arrive penchée. Si vous vous souvenez de vos cours de collège, c'est le même principe qui explique les saisons. La lumière du soleil est proche de la verticale en été, mais est de plus en plus penché quand on s'avance vers l'Hiver. La lumière solaire est donc étalée sur une surface plus grande, ce qui fait qu'un point de la surface recevra moins de lumière, celle-ci étant diluée, étalée. [[File:Radiación solar.png|centre|vignette|upright=2|Exemple avec la lumière solaire.]] [[File:Angle of incidence.svg|vignette|upright=1|Angle d'incidence.]] En clair, tout dépend de l''''angle d'incidence''' de la lumière. Reste à voir comment calculer cet angle. La lumière incidente est définie par un vecteur, qui part de la source de lumière et atterrit sur le sommet considéré. Imaginez simplement que ce vecteur suit un rayon lumineux provenant de la source de lumière. Le vecteur pour la lumière incidente sera noté L. L'angle d'incidence est l'angle que fait ce vecteur avec la verticale de la surface, au niveau du sommet considéré. [[File:Graphics lightmodel ptsource.png|vignette|Normale de la surface.]] Pour cela, les calculs d'éclairage ont besoin de connaitre la verticale d'un sommet. Un sommet est donc associé à un vecteur, appelé la '''normale''', qui indique la verticale en ce point. Deux sommets différents peuvent avoir deux normales différentes, même s'ils sont proches. Elles sont d'autant plus différentes que la surface est rugueuse, non-lisse. La normale est prédéterminée lors de la création du modèle 3D, il n'y a pas besoin de le calculer. Par contre, elle est modifiée lors de l'étape de transformation, quand on place le modèle 3D dans la scène 3D. Les deux autres vecteurs sont à calculer à chaque image, car ils changent quand on bouge le sommet. La lumière qui arrive sur la surface dépend de l'angle entre la normale et le vecteur L. Précisément, elle dépend du cosinus de cet angle. En multipliant ce cosinus avec l'intensité de la lumière, on a la lumière arrivante. La couleur finale d'un pixel est donc : : <math>\text{Couleur finale} = I \times \cos{(N, L)} \times BRDF(...)</math> Le terme <math>I \times \cos{N, L}</math> ne dépend pas de la surface considérée. Juste de la position de la source de lumière, de la position du sommet et de son orientation par rapport à la lumière. Aussi, il est parfois appelé le '''terme géométrique''', en opposition aux propriétés de la surface. Les propriétés de la surface sont définies par un '''''material''''', qui indique comment il réfléchit la lumière, ainsi que sa texture. ===Le produit scalaire de deux vecteurs=== Calculer le terme géométrique demande de calculer le cosinus d'un angle. Et il n'est pas le seul : les autres calculs d'éclairage que nous allons voir demandent de calculer des cosinus. Or, les calculs trigonométriques sont très gourmands pour le GPU. Pour éviter le calcul d'un cosinus, les GPU utilisent une opération mathématique appelée le ''produit scalaire''. Le produit scalaire agit sur deux vecteurs, que l'on notera A et B. Un produit scalaire prend : la longueur des deux vecteurs, et l'angle entre les deux vecteurs noté <math>\omega</math>. Le produit scalaire est équivalent à la formule suivante : : <math>\text{Produit scalaire de deux vecteurs A et B} = \vec{A} \cdot \vec{B} = A \times B \times \cos{(\omega)}</math>, avec A et B la longueur des deux vecteurs A et B. L'avantage est que le produit scalaire se calcule simplement avec des additions, soustractions et multiplications, des opérations que les cartes graphiques savent faire très facilement. Le produit scalaire de deux vecteurs de coordonnées x,y,z est le suivant : : <math>\vec{A} \cdot \vec{B} = x_A \times x_B + y_A \times y_B + z_A \times z_B</math> En clair, on multiplie les coordonnées identiques, et on additionne les résultats. Rien de compliqué. Un avantage est que tous les vecteurs vus précédemment sont normalisés, à savoir qu'ils ont une longueur qui vaut 1. Ainsi, le calcul du produit scalaire devient équivalent au calcul du produit scalaire. ===La réflexion de la lumière sur la surface=== [[File:Ray Diagram 2.svg|vignette|Reflection de la lumière sur une surface parfaitement lisse.]] Maintenant que nous venons de voir le terme géométrique, voyons le BRDF, qui définit comment la surface de l'objet 3D réfléchit la lumière. Vos cours de collège vous ont sans doute appris que la lumière est réfléchie avec le même angle d'arrivée. L'angle d'incidence et l'angle de réflexion sont égaux, comme illustré ci-contre. On parle alors de '''réflexion parfaite'''. Mais cela ne vaut que pour une surface parfaitement lisse, comme un miroir parfait. Dans la réalité, une surface a tendance à renvoyer des rayons dans toutes les directions. La raison est qu'une surface réelle est rugueuse, avec de petites aspérités et des micro-reliefs, qui renvoient la lumière dans des directions "aléatoires". La lumière « rebondit » sur la surface de l'objet et une partie s'éparpille dans un peu toutes les directions. On parle alors de '''réflexion diffuse'''. {| |- |[[File:Dioptre reflexion diffuse speculaire refraction.svg|vignette|upright=1.4|Différence entre réflexion diffuse et spéculaire.]] |[[File:Diffuse reflection.svg|vignette|upright=1|Réflexion diffuse.]] |} Maintenant, imaginons que la surface n'ait qu'une réflexion diffuse, pas d'autres formes de réflexion. Et imaginons aussi que cette réflexion diffuse soit parfaite, à savoir que la lumière réfléchie soit renvoyée à l'identique dans toutes les directions, sans aucune direction privilégiée. On a alors le ''material'' le plus simple qui soit, appelé un '''''diffuse material'''''. Vu que la lumière est réfléchie à l'identique dans toutes les directions, elle sera identique peu importe où on place la caméra. La lumière finale ne dépend donc que des propriété de la surface, que de sa couleur. En clair, il suffit de donner une '''couleur diffuse''' à chaque sommet. La couleur diffuse est simplement multipliée par le terme géométrique, pour obtenir la lumière réfléchie finale. Rien de plus, rien de moins. Cela donne l'équation suivante, avec les termes suivants : * L est le vecteur pour la lumière incidente ; * N est la normale du sommet ; * I est l'intensité de la source de lumière ; * <math>C_d</math> est la couleur diffuse. : <math>\text{Illumination diffuse} = C_d \times \left[ I \times (\vec{N} \cdot \vec{L}) \right]</math> Rajoutons maintenant l'effet de la lumière ambiante à un ''material'' de ce genre. Pour rappel, la lumière ambiante vient de toutes les directions à part égale, ce qui fait que son angle d'incidence n'a donc pas d'effet. L'intensité de la lumière ambiante est déterminée lors de la création de la scène 3D, c'est une constante qui n'a pas à être calculée. Pour obtenir l'effet de la lumière ambiante sur un objet, il suffit de multiplier sa couleur diffuse par l'intensité de la lumière ambiante. Cependant, de nombreux moteurs de jeux ajoutent une '''couleur ambiante''', différente de la couleur diffuse. : <math>\text{Illumination ambiante} = C_a \times I_a</math> avec <math>C_a</math> la couleur ambiante du point de surface et <math>I_a</math> l'intensité de la lumière ambiante. En plus de la réflexion diffuse parfaite, de nombreux matériaux ajoutent une '''réflexion spéculaire''', qui n'est pas exactement la réflexion parfaite, en est très proche. Les rayons réfléchis sont très proches de la direction de réflexion parfaite, et s'atténuent très vite en s'en éloignant. Le résultat ressemble à une sorte de petit "point blanc", très lumineux, orienté vers la source de lumière, appelé le '''''specular highlight'''''. La réflexion diffuse est prédominante pour les matériaux rugueux, alors que la réflexion spéculaire est dominante sur les matériaux métalliques ou très lisses. [[File:Phong components version 4.png|centre|vignette|upright=3.0|Couleurs utilisées dans l'algorithme de Phong.]] [[File:Phong Vectors.svg|vignette|Vecteurs utilisés dans l'algorithme de Phong (et dans le calcul de l'éclairage, de manière générale).]] Pour calculer la réflexion spéculaire, il faut d'abord connaitre le vecteur pour la réflexion parfaite, que nous noterons R dans ce qui suit. Le vecteur R peut se calculer avec la formule ci-dessous : : <math>\vec{R} = 2 (\vec{L} \cdot \vec{N}) \times \vec{N} - \vec{L} </math> La réflexion spéculaire dépend de l'angle entre la direction du regard et la normale : plus celui-ci est proche de l'angle de réflexion parfaite, plus la réflexion spéculaire sera intense. Le vecteur pour la direction du regard sera noté V, pour vue ou vision. La réflexion spéculaire est une fonction qui dépend de l'angle entre les vecteurs R et V. Le calcul de la réflexion spéculaire utilise une '''couleur spéculaire''', qui est l'équivalent de la couleur diffuse pour la réflexion spéculaire. : <math>\text{BRDF spéculaire} = C_s \times f(\vec{R} \cdot \vec{V}) </math> La fonction varie grandement d'un modèle de calcul spéculaire à l'autre. Aussi, je ne rentre pas dans le détail. L'essentiel est que vous compreniez que le calcul de l'éclairage utilise de nombreux calculs géométriques, réalisés avec des produits scalaires. Les calculs géométriques utilisent la couleur d'un sommet, la normale du sommet, et le vecteur de la lumière incidente. Les autres informations sont calculées à l'exécution. ===Les algorithmes d'éclairage basiques : par triangle, par sommet et par pixel=== Dans tout ce qui a été dit précédemment, l'éclairage est calculé pour chaque sommet. Il attribue une illumination/couleur à chaque sommet de la scène 3D, ce qui fait qu'on parle d''''éclairage par sommet''', ou ''vertex lighting''. Il est assez rudimentaire et donne un éclairage très brut, mais il peut être réalisé avant l'étape de rastérisation. Mais une fois qu'on a obtenu la couleur des sommets, reste à colorier les triangles. Et pour cela, il y a deux manières de faire, qui sont appelées l'éclairage plat et l'éclairage de Gouraud. [[File:D3D Shading Triangles.png|vignette|Dans ce dessin, le triangle a un sommet de couleur bleu foncé, un autre de couleur rouge et un autre de couleur bleu clair. L’interpolation plate et de Gouraud donnent des résultats bien différents.]] L''''éclairage plat''' calcule l'éclairage triangle par triangle. Il y a plusieurs manières de faire pour ça, mais la plus simple colorie un triangle avec la couleur moyenne des trois sommets. Une autre possibilité fait les calculs d'éclairage triangle par triangle, en utilisant une normale par triangle et non par sommet, idem pour les couleurs ambiante/spéculaire/diffuse. Mais c'est plus rare car cela demande de placer la normale quelque part dans le triangle, ce qui rajoute des informations. L''''éclairage de Gouraud''' effectue lui aussi une moyenne de la couleur de chaque sommet, sauf que celle-ci est pondérée par la distance du sommet avec le pixel. Plus le pixel est loin d'un sommet, plus son coefficient est petit. Typiquement, le coefficient varie entre 0 et 1 : de 1 si le pixel est sur le sommet, à 0 si le pixel est sur un des sommets adjacents. La moyenne effectuée est généralement une interpolation bilinéaire, mais n'importe quel algorithme d'interpolation peut marcher, qu'il soit simplement linéaire, bilinéaire, cubique, hyperbolique. L'étape d'interpolation est prise en charge par l'étape de rastérisation, qui effectue cette moyenne automatiquement. L'éclairage par sommet a eu son heure de gloire, mais il est maintenant remplacé par l''''éclairage par pixel''' (''per-pixel lighting''), qui calcule l'éclairage pixel par pixel. En clair, l’éclairage est finalisé après l'étape de rastérisation, il ne se fait pas qu'au niveau de la géométrie. Il existe plusieurs types d'éclairage par pixel, mais on peut les classer en deux grands types : l'éclairage de Phong et le ''bump/normal mapping''. L''''éclairage de Phong''' calcule l'éclairage pixel par pixel. Avec cet algorithme, la géométrie n'est pas éclairée : les couleurs des sommets ne sont pas calculées. A la place, les normales sont envoyées à l'étape de rastérisation, qui effectue une opération d'interpolation, qui renvoie une normale pour chaque pixel. Les calculs d'éclairage utilisent alors ces normales pour faire les calculs d'éclairage pour chaque pixel. La technique du '''''normal mapping''''' est assez simple à expliquer, sans compter que plusieurs cartes graphiques l'ont implémentée directement dans leurs circuits. Là où l'éclairage de Phong interpole les normales pour chaque pixel, le ''normal-mapping'' précalcule les normales d'une surface dans une texture, appelée la ''normal-map''. Lors des calculs d'éclairage, la carte graphique lit les normales adéquates directement depuis cette texture, puis fait les calculs d'éclairage avec. [[File:WallSimpleAndNormalMapping.png|centre|vignette|upright=2|Différence sans et avec ''normal-mapping''.]] Avec cette technique, l'éclairage n'est pas géré par pixel, mais par texel, ce qui fait qu'il a une qualité de rendu un peu inférieure à un vrai éclairage de Phong, mais bien supérieure à un éclairage par sommet. Par contre, les techniques de ''normal mapping'' permettent d'ajouter du relief et des détails sur des surfaces planes en jouant sur l'éclairage. Elles permettent ainsi de simplifier grandement la géométrie rendue, tout en utilisant l'éclairage pour compenser. [[File:Bump mapping.png|centre|vignette|upright=2|Bump mapping]] L'éclairage par pixel a une qualité d'éclairage supérieure aux techniques d'éclairage par sommet, mais il est aussi plus gourmand. L'éclairage par pixel est utilisé dans presque tous les jeux vidéo depuis DOOM 3, en raison de sa meilleure qualité, mais cela n'aurait pas été possible si le matériel n'avait pas évolué de manière à incorporer des algorithmes d'éclairage matériel assez puissants, avant de basculer sur un éclairage programmable. La différence entre l'éclairage par pixel et par sommet se voit assez facilement à l'écran. L'éclairage plat donne un éclairage assez carré, avec des frontières assez nettes. L'éclairage de Gouraud donne des ombres plus lisses, dans une certaine mesure, mais pèche à rendre correctement les reflets spéculaires. L'éclairage de Phong est de meilleure qualité, surtout pour les reflets spéculaires. es trois algorithmes peuvent être implémentés soit dans la carte graphique, soit en logiciel. Nous verrons comment les cartes graphiques peuvent implémenter ces algorithmes, dans les deux prochains chapitres. {| |- |[[File:Per face lighting.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting.png|vignette|upright=1|Phong Shading]] |- |[[File:Per face lighting example.png|vignette|upright=1|Flat shading]] |[[File:Per vertex lighting example.png|vignette|upright=1|Gouraud Shading]] |[[File:Per fragment lighting example.png|vignette|upright=1|Phong Shading]] |} ===Les ''shaders'' : des programmes exécutés sur le GPU=== Maintenant que nous venons de voir les algorithmes d'éclairages, il est temps de voir comment les réaliser sur une carte graphique. Nous venons de voir qu'il y a une différence entre l'éclairage par pixel et par sommet. Intuitivement, l'éclairage par sommet devrait se faire avec les calculs géométriques, alors que l'éclairage par pixel devrait se faire après avoir appliqué les textures. Les toutes premières cartes graphiques ne géraient ni l'éclairage par sommet, ni l'éclairage par pixel. Elles laissaient les calculs géométriques au CPU. Par la suite, la Geforce 256 a intégré '''circuit de ''Transform & Lightning''''', qui s'occupait de tous les calculs géométriques, éclairage par sommet inclus (d'où le L de T&L). Elle gérait alors l'éclairage par sommet, mais un algorithme particulier, qui n'était pas très flexible. Il ne gérait que des ''material'' bien précis (des ''Phong materials''), rien de plus. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | Unité de T&L : géométrie | Rastérisation | Placage de textures | ''Raster Operations Pipeline'' |} L'amélioration suivante est venue sur la Geforce 3 : l'unité de T&L est devenue programmable. Au vu le grand nombre d'algorithmes d'éclairages possibles et le grand nombre de ''materials'' possibles, c'était la seule voie possibles. Les programmeurs pouvaient programmer leurs propres algorithmes d'éclairage par sommet, même s'ils devaient aussi programmer les étapes de transformation et de projection. Mais nous détaillerons cela dans un chapitre dédié sur l'historique des GPUs. Ce qui est important est que la Geforce 3 a introduit une fonctionnalité absolument cruciale pour le rendu 3D moderne : les '''''shaders'''''. Il s'agit de programmes informatiques exécutés par la carte graphique, qui servaient initialement à coder des algorithmes d'éclairage. D'où leur nom : ''shader'' pour ''shading'' (éclairage en anglais). Cependant, l'usage modernes des shaders dépasse le cadre des algorithmes d'éclairage. L'avantage est que cela simplifie grandement l'implémentation des algorithmes d'éclairage. Pas besoin de les intégrer dans la carte graphique pour les utiliser, pas besoin d'un circuit distinct pour chaque algorithme. Sans shaders, si la carte graphique ne gère pas un algorithme d'éclairage, on ne peut pas l'utiliser. A la rigueur, il est parfois possible de l'émuler avec des contournements logiciels, mais au prix de performances souvent désastreuses. Avec des shaders, il est possible de programmer l'algorithme d'éclairage de notre choix, pour l'exécuter sur la carte graphique, avec des performances plus que convenables. [[File:Implémentation de l'éclairage sur les cartes graphiques.png|vignette|Implémentation de l'éclairage sur les cartes graphiques]] Il existe plusieurs types de shaders, mais les deux principaux sont les '''''vertex shaders''''' et les '''''pixel shaders'''''. Les pixels shaders s'occupent de l'éclairage par pixel, leur nom est assez parlent. Les vertex shaders s'occupent de l'éclairage par sommet, mais aussi des étapes de transformation/projection. Je parle bien des trois étapes de transformation vues plus haut, qui effectuent des calculs de transformation de coordonnées avec des matrices. La raison à cela est que les calculs de transformation ressemblent beaucoup aux calculs d'éclairage par sommet. Ils impliquent tous deux des calculs vectoriels, comme des produits scalaires et des produits vectoriels, qui agissent sur des sommets/triangles. Si la carte graphique incorpore un processeur de shader capable de faire de tels calculs, alors il peut servir pour les deux. Pour implémenter les shaders, il a fallu ajouter des processeurs à la carte graphique. Les processeurs en question exécutent les shaders, ils peuvent lire ou écrire dans des textures, mais ne font rien d'autres. Les ''vertex shaders'' font tout ce qui a trait à la géométrie, ils remplacent l'unité de T&L. Les pixels shaders sont entre la rastérisation et les ROPs, ils sont très liés à l'unité de texture. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Vertex shader'' | rowspan="2" | Rastérisation | Placage de textures | rowspan="2" |''Raster Operations Pipeline'' |- | class="f_rouge" | ''Pixel shader'' |} {{NavChapitre | book=Les cartes graphiques | prev=Les cartes d'affichage des anciens PC | prevText=Les cartes d'affichage des anciens PC | next=Avant les GPUs : les cartes accélératrices 3D | nextText=Avant les GPUs : les cartes accélératrices 3D }}{{autocat}} 5jl18pymjov7sacs8or1imzyncq5msk Les cartes graphiques/Le pipeline géométrique d'un GPU 0 79241 763793 763569 2026-04-16T18:52:34Z Mewtow 31375 /* L'étape d’assemblage de primitives est dupliquée */ 763793 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. Mais d'autres processeurs utilisaient la fonctionnalité de ''stream ouput'' à la place, à savoir que les résultats d'un ''geometry shader'' sont simplement mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. La conséquence est que le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. ===L'étape d’assemblage de primitives est dupliquée=== J'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> c2yikw762rq0e45k8ve92mo084out2i 763794 763793 2026-04-16T18:55:26Z Mewtow 31375 /* La conservation de l'ordre des sommets entrants et sortants */ 763794 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. Mais d'autres processeurs utilisaient la fonctionnalité de ''stream ouput'' à la place, à savoir que les résultats d'un ''geometry shader'' sont simplement mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. La conséquence est que le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===L'étape d’assemblage de primitives est dupliquée=== J'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, non-regroupés en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader'', pour déterminer les primitives finales. Et il faut aussi refaire le ''culling'', au cas où les primitives générées ne soient pas visibles depuis la caméra. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> 3k76edv9pxdjeu2h4i4bui8j9x4z9qp 763795 763794 2026-04-16T18:56:25Z Mewtow 31375 /* L'étape d’assemblage de primitives est dupliquée */ 763795 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. Mais d'autres processeurs utilisaient la fonctionnalité de ''stream ouput'' à la place, à savoir que les résultats d'un ''geometry shader'' sont simplement mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. La conséquence est que le nombre d'instance exécutables en parallèle est rapidement limitée par le nombre de sommets maximal que peut sortir le ''geometry shader'', nombre qui est rarement atteint en pratique. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===L'étape d’assemblage de primitives est dupliquée=== J'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> 8yp65y5x9f0djyb5ay8vtog5tniirtz 763796 763795 2026-04-16T19:00:22Z Mewtow 31375 /* La conservation de l'ordre des sommets entrants et sortants */ 763796 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. Mais sur d'autres GPU, les résultats d'un ''geometry shader'' sont simplement mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Le GPU n'a pas de choix que de mettre en attente certaines instances de ''geometry shader'', ils doivent attendre pour s'exécuter, même si des processeurs de shaders sont inoccupés. Les processeurs de shaders sont donc sous-utilisés. Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===L'étape d’assemblage de primitives est dupliquée=== J'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> avocpg12u4mjck36bj97lflbkt5d9sy 763797 763796 2026-04-16T19:06:22Z Mewtow 31375 /* La conservation de l'ordre des sommets entrants et sortants */ 763797 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. Mais sur d'autres GPU, les résultats d'un ''geometry shader'' sont simplement mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Une solution à cela est la suivante. Quand un ''geometry shader'' a terminé son travail, il regarde s'il y a de la place dans le second tampon de primitives. Si celui-ci est plein, il attend que de la place se libère. On a donc un processeur de shader qui ne fait rien. les primitives calculées sont juste mémorisées dans les registres en attendant d'être transférées au tampon de primitives. Au pire, on peut espérer qu'une autre instance s'exécute dans un autre ''thread'', grâce aux propriétés de ''multithreading'' matériel. Le nombre de ''geometry shader'' pouvant attendre est alors limité par le nombre de registres du processeur, et la taille des ''shaders''. Avoir beaucoup de registres est alors un avantage ([http://www.joshbarczak.com/blog/?p=667 Why Geometry Shaders Are Slow (Unless you’re Intel)]). Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===L'étape d’assemblage de primitives est dupliquée=== J'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> pezuxy3fzygv8jw9a00o7qs8pdbm5e2 763798 763797 2026-04-16T19:09:08Z Mewtow 31375 /* DirectX 10 : les geometry shaders */ 763798 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==DirectX 10 : les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Au passage, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===Les complications liées à la sortie des ''geometry shaders''=== J'ai dit plus haut que le GPu incorpore un second tampon de primitives. Mais sur quelques GPU, les résultats d'un ''geometry shader'' ne passent pas directement par un second tampon de primitives. A la place, ils sont mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Une solution à cela est la suivante. Quand un ''geometry shader'' a terminé son travail, il regarde s'il y a de la place dans le second tampon de primitives. Si celui-ci est plein, il attend que de la place se libère. On a donc un processeur de shader qui ne fait rien. les primitives calculées sont juste mémorisées dans les registres en attendant d'être transférées au tampon de primitives. Au pire, on peut espérer qu'une autre instance s'exécute dans un autre ''thread'', grâce aux propriétés de ''multithreading'' matériel. Le nombre de ''geometry shader'' pouvant attendre est alors limité par le nombre de registres du processeur, et la taille des ''shaders''. Avoir beaucoup de registres est alors un avantage ([http://www.joshbarczak.com/blog/?p=667 Why Geometry Shaders Are Slow (Unless you’re Intel)]). Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> b1n4tdhed8t4zoscnvdhtucf3xcpcea 763799 763798 2026-04-16T19:19:44Z Mewtow 31375 /* DirectX 10 : les geometry shaders */ 763799 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==Les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Au passage, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===Les complications liées à la sortie des ''geometry shaders''=== J'ai dit plus haut que le GPu incorpore un second tampon de primitives. Mais sur quelques GPU, les résultats d'un ''geometry shader'' ne passent pas directement par un second tampon de primitives. A la place, ils sont mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Une solution à cela est la suivante. Quand un ''geometry shader'' a terminé son travail, il regarde s'il y a de la place dans le second tampon de primitives. Si celui-ci est plein, il attend que de la place se libère. On a donc un processeur de shader qui ne fait rien. les primitives calculées sont juste mémorisées dans les registres en attendant d'être transférées au tampon de primitives. Au pire, on peut espérer qu'une autre instance s'exécute dans un autre ''thread'', grâce aux propriétés de ''multithreading'' matériel. Le nombre de ''geometry shader'' pouvant attendre est alors limité par le nombre de registres du processeur, et la taille des ''shaders''. Avoir beaucoup de registres est alors un avantage ([http://www.joshbarczak.com/blog/?p=667 Why Geometry Shaders Are Slow (Unless you’re Intel)]). Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==DirectX 12 : les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> 9n428yy9k20q4cn1hni0ddt2ybrwiui 763800 763799 2026-04-16T19:19:51Z Mewtow 31375 /* DirectX 12 : les mesh shaders */ 763800 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==Les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Au passage, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===Les complications liées à la sortie des ''geometry shaders''=== J'ai dit plus haut que le GPu incorpore un second tampon de primitives. Mais sur quelques GPU, les résultats d'un ''geometry shader'' ne passent pas directement par un second tampon de primitives. A la place, ils sont mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Une solution à cela est la suivante. Quand un ''geometry shader'' a terminé son travail, il regarde s'il y a de la place dans le second tampon de primitives. Si celui-ci est plein, il attend que de la place se libère. On a donc un processeur de shader qui ne fait rien. les primitives calculées sont juste mémorisées dans les registres en attendant d'être transférées au tampon de primitives. Au pire, on peut espérer qu'une autre instance s'exécute dans un autre ''thread'', grâce aux propriétés de ''multithreading'' matériel. Le nombre de ''geometry shader'' pouvant attendre est alors limité par le nombre de registres du processeur, et la taille des ''shaders''. Avoir beaucoup de registres est alors un avantage ([http://www.joshbarczak.com/blog/?p=667 Why Geometry Shaders Are Slow (Unless you’re Intel)]). Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ===La fonctionnalité de ''stream output''=== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==Les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> lu5sqgjdo3f1h4oktv2sx8g6vwjozec 763812 763800 2026-04-16T20:24:27Z Mewtow 31375 /* La fonctionnalité de stream output */ Déplacement dans un autre chapitre 763812 wikitext text/x-wiki Dans le chapitre précédent, nous avons vu qu'il y a une différence entre le pipeline géométrique des anciennes stations de travail et des ordinateurs personnels. Les premiers tendaient à utiliser des processeurs flottants, programmés avec un ''firmware/microcode'' non-modifiable. Les ordinateurs personnels ont eu commencé avec des circuits géométriques fixe, pour les rendre de plus en plus programmables. Dans ce chapitre, nous allons étudier les circuits géométriques d'un GPU d'ordinateur personnel, et voir comment ils ont évolués dans le temps. ==Le ''vertex pipeline''== Les premières cartes graphiques ne traitaient que des sommets, les primitives n'apparaissaient qu'à l'étape de rastérisation. Leur pipeline a progressivement évolué pour pouvoir exécuter des ''shaders'' sur des primitives, mais ce n'est apparu qu'avec DirectX 10. Avant, les unités géométriques ne géraient que des sommets. Nous allons voir de telles unités géométriques ici. Elles sont composées de trois circuits : l'''input assembly'', l'unité géométrique proprement dit, et l'assemblage des primitives. {|class="wikitable" |- ! colspan="4" | Cartes accélératrices PC, avant l'arrivée des ''shaders'' |- | rowspan="2" class="f_rouge" | ''Input assembly'' | ''Transform & Lighting'' | rowspan="2" class="f_rouge" | ''Primitive assembly'' |- | ''Vertex shader'' |} Pour comprendre à quoi servent l'''input assembler'' et l'assemblage de primitives, il faut parler de certaines optimisations présentes sur les cartes graphiques de l'époque. ===Les représentations des maillages : les optimisations=== Les optimisations visaient à réduire la mémoire prise pour les objets 3D. Pour rappel, les objets géométriques et la scène 3D sont mémorisés dans la mémoire vidéo, avec un assemblage de triangles collés les uns aux autres, l'ensemble formant un '''maillage'''. Pour mémoriser un maillage, il suffit d'utiliser une liste de triangles, chaque triangle étant définit par trois sommets consécutifs. Cependant, utiliser cette représentation gaspille beaucoup de mémoire ! [[File:Représentation naive d'un maillage 3D.png|centre|vignette|upright=2|Représentation naive d'un maillage 3D]] [[File:Cube colored.png|vignette|Cube en 3D]] Pour comprendre pourquoi, il faut savoir qu'un sommet est très souvent partagé par plusieurs triangles. Pour comprendre pourquoi, prenons l'exemple du cube de l'image ci-contre. Le sommet rouge du cube appartient aux 3 faces grise, jaune et bleue, et sera présent en trois exemplaires dans le tampon de sommets : un pour la face bleue, un pour la jaune, et un pour la grise. Et si vous croyez que l'exemple du cube n'est pas réaliste, voici un chiffre obtenu empiriquement, par analyse de maillages utilisés dans un JV : en moyenne, un sommet est dupliqué en 6 exemplaires. Pour éviter ce gâchis, les concepteurs d'API et de cartes graphiques ont inventé des représentations pour les maillages, qui visent éliminer cette redondance. Nous les appellerons des '''représentations compressées''', bien que ce terme soit un peu trompeur. Mais dans les faits, il s'agit bien d'une forme de compression de données, bien que très différente de celle utilisée pour compresser un fichier, de la vidéo, du texte ou de l'audio. La liste de triangle est en quelque sorte compressée lors de la création du maillage, puis décompressée par le matériel. Les représentations compressées n'utilisent pas une liste de triangles, mais une liste de sommets. La liste de sommets est mémorisée en mémoire vidéo, et s'appelle le '''tampon de sommets'''. Ainsi, un sommet présent dans plusieurs triangles n'est mémorisé qu'une seule fois, ou presque. Reste à reconstituer les triangles à partir de cette liste de sommets. Et c'est le travail de l'''input assembler'' et l'assemblage de primitive, justement. Mais avant de comprendre ce qu'ils font, nous devons voir les représentations compressées utilisées sur les cartes graphiques de l'époque. Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles ont été remplacées par la représentation indicée, apparue avec Direct X 7 et les versions équivalentes d'Open GL. Nous allons voir cette dernière en premier, car elle est plus simple. La '''représentation indicée''' stocke les triangles et les sommets séparément, avec une liste de triangle séparée de la liste de sommets. Dit comme cela, on ne voit pas vraiment où se trouve le gain en mémoire. Mais il y a une astuce, qui tient à ce qu'on met dans la liste de triangles. Les sommets sont numérotés, le numéro indiquant leur place dans la liste de sommets. Dans la liste de triangles, un triangle est mémorisé non pas par trois sommets consécutifs, mais par trois numéros de sommets. Le numéro est aussi appelé l'indice du sommet, et la liste de triangles est appelée le ''tampon d'indices''. : Le terme '"indice" devrait rappeler quelques chose à ceux qui savent ce qu'est un tableau en programmation. Le résultat est que les sommets ne sont pas dupliqués, mais on doit ajouter un tampon d'indice pour compenser. L'astuce est que l'économie en termes de sommets dépasse largement l'ajout du tampon d'indice. En effet, un indice prend moins de place qu'un sommet. Un sommet demande trois coordonnées, une couleur de sommet, des coordonnées de texture, une normale et bien d'autres attributs de sommets. En comparaison, un indice est un simple numéro, un nombre entier. En moyenne, un sommet prend 10 fois plus de place qu'un indice. Si on fait le compte, au lieu d'avoir N copies d'un sommet, on a juste une seule copie et N indices. L'économie liée à la taille des indices l'emporte. : On pourrait remplacer les indices par des pointeurs, ce qui donnerait un cas particulier d'une structure de données connue sous le nom de vecteur de Liffe. Mais ce n'est pas très pratique et n'est pas utilisé dans le domaine du rendu 3D. Un numéro entier est plus court qu'un pointeur complet. [[File:Représentation indicée d'un maillage 3D.png|centre|vignette|upright=2|Représentation indicée d'un maillage 3D]] Les premières versions d'Open GL et Direct X implémentaient deux représentations compressées : les ''triangle fans'' et celle des ''triangle strips''. Elles sont plus complexes, mais permettent une économie de mémoire encore plus importante. La technique des '''triangles fan''' était la moins utilisée des deux, mais elle est plus simple à expliquer, ce qui fait que je commence avec elle. Elle permet de dessiner des triangles qui partagent un sommet unique, ce qui donne une forme soit circulaire, soit en forme d'éventail. Les ''triangles fans'' sont utiles pour créer des figures comme des cercles, des halos de lumière, etc. Un triangle est définit par le sommet partagé, puis deux sommets. Le sommet partagé n'est présent qu'en un seul exemplaire, et une autre optimisation permet d'optimiser les deux autres sommets. [[File:Triangle fan.png|centre|vignette|upright=2.0|Triangle fan]] Avec cette représentation, le tampon de sommets contient une liste de sommets, qui est interprétée sommet par sommet. Le premier sommet est le sommet partagé par tous les triangles du ''triangle fan''. Le premier triangle est définit par le sommet partagé et deux nouveaux sommets. Les triangles suivants sont eux définit par un seul sommet, pas deux. En effet, deux triangles consécutifs partagent une arête, définie par le sommet partagé et un des deux sommets. Sur les deux sommets, le dernier sommet est celui de l'arête partagée. En faisant ainsi, un triangle est définit par un nouveau sommet, le sommet précédent dans le tampon de sommet, et le sommet partagé. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! Triangle 7 !! ... |- | Sommet 1 || X || X || X || X || X || X || X || X |- | Sommet 2 || X || || || || || || |- | Sommet 3 || X || X || || || || || |- | Sommet 4 || || X || X || || || || |- | Sommet 5 || || || X || X || || || |- | Sommet 6 || || || || X || X || || |- | Sommet 7 || || || || || X || X || |- | Sommet 8 || || || || || || X || X |} La technique des '''triangles strip''' optimise le rendu de triangles placés en série, comme illustré dans le schéma ci-dessous. Notez que deux consécutifs ont deux sommets en commun. L'idée est alors que quand on passe au triangle suivant, on ne précise que le sommet restant, pas les deux sommets en commun. [[File:Triangle strip.svg|centre|vignette|upright=2|Triangle strip]] L'implémentation est assez simple : dans le tampon de sommets, trois sommets consécutifs forment un triangle. Et pour passer d'un triangle au suivant, on ne saute pas de trois sommets, on passe d'un sommet au suivant. {|class="wikitable" |- ! Tampon de sommet !! Triangle 1 !! Triangle 2 !! Triangle 3 !! Triangle 4 !! Triangle 5 !! Triangle 6 !! ... |- | Sommet 1 || X || || || || || |- | Sommet 2 || X || X || || || || |- | Sommet 3 || X || X || X || || || |- | Sommet 4 || || X || X || X || || |- | Sommet 5 || || || X || X || X || |- | Sommet 6 || || || || X || X || X |- | Sommet 7 || || || || || X || X |- | Sommet 8 || || || || || || X |} Les ''triangle fan'' et ''triangle strip'' permettent une économie de mémoire conséquente, comparé à la représentation non-compressée. Au lieu de trois sommets pour chaque triangle, on se retrouve avec un sommet pour chaque triangle, plus les deux premiers sommets. La comparaison avec l'usage d'un tampon d'indice dépend de la taille des indices, mais ''triangle fan'' et ''triangle strip'' sont plus économes niveau mémoire vidéo. Un problème est que les ''triangle strip'' ne permettent pas de représenter tous les modèles 3D, certains ne sont simplement pas compatibles avec cette représentation. Et pour les ''triangle fan'', c'est encore pire ! Cependant, il est souvent possible de ruser, ce qui permet de faire rentrer des modèles non-coopératifs dans un ''triangle strip'', mais quelques sommets sont alors redondants. ===L'''input assembler'' et le tampon d'indice=== Les représentations précédentes ont une influence importante sur le pipeline géométrique. Pour les gérer, il a fallu non seulement modifier l'assemblage de primitives, mais aussi rajouter un circuit juste avant l'unité géométrique : l'''input assembler''. Il charge les sommets depuis la mémoire vidéo, pour les injecter dans le reste du pipeline. [[File:Input assembler.png|centre|vignette|upright=2.0|Input assembler]] Pour faire son travail, il a besoin de l'adresse des données géométriques en mémoire, leur taille et éventuellement du type des données qu'on lui envoie (sommets codées sur 32 bits, 64, 128, etc). En clair, il doit connaitre l'adresse du tampon de sommet et éventuellement celle du tampon d'indice. Et en général, c'est une unité d'accès mémoire un peu particulière, qui contient des circuits assez classiques pour ce genre de circuits : des circuits de calcul d'adresse, des circuits pour commander la mémoire VRAM, un contrôleur mémoire, diverses mémoires tampons, etc. Il procède différemment suivant la représentation utilisée. Il peut lire trois sommets consécutifs avec une représentation non-compressée, il peut lire un tampon d'indice et l'utiliser pour charger les sommets adéquats, il peut lire un sommet à la fois avec les ''triangle fan/strip'', etc. Tout dépend de comment l'unité est configurée. Dans ce qui suit, nous allons étudier un ''input assembler'' qui gère la représentation indicée. Il peut être adapté pour gérer les autres représentations assez simplement. L'idée est que l'''input assembler'' est composé de trois circuits principaux : un qui lit le tampon d'indice, un autre qui lit le tampon de sommets, un dernier qui package les sommets. Le premier lit les indices depuis la mémoire vidéo. Le second récupère l'indice chargé par le premier, et lit le sommet associé dans le tampon de sommets. Ils sont respectivement appelés avec les noms : ''index fetch'' et ''vertex fetch''. Le dernier circuit se contente de formater les sommets pour qu'ils soient compréhensibles par les unités géométriques. [[File:Implémentation matérielle de l'input assembler.png|centre|vignette|upright=2|Implémentation matérielle de l'input assembler.]] Pour les représentations autres qu'indicée, seul le ''vertex fetch'' est utilisé. Il se contente alors de balayer le tampon de sommets dans l'ordre, du premier sommet au dernier. Un vulgaire compteur d'adresse suffit pour cela. Avec la représentation indicée, le circuit d'''index fetch'' est utilisé. Il balaye un tableau d'indices du début à la fin, ce qui fait que le calcul d'adresse est réalisé par un simple compteur d'adresse. Le circuit de ''vertex fetch'' fait des calculs d'adresse un chouilla moins simples, mais qui se contentent de combiner l'adresse du tampon de sommets avec l'indice. Les unités de ''index fetch'' et de ''vertex fetch'' font donc des calculs d'adresse et des accès mémoire. Par contre, les deux circuits peuvent implémenter des mémoires caches, pour améliorer les performances. Vous remarquerez que l’''input assembler'' fait surtout des calculs d'adresse, des lectures en mémoire, et des conversions de format de données. Un processeur de ''vertex shader'' peut faire la même chose, ce qui fait qu'il est possible d'émuler l'''input assembler'' avec un ''vertex shader''. La seule condition, absolument nécessaire, est que le ''vertex shader'' puisse lire des données en mémoire vidéo. Et pas seulement lire des textures, comme le permettent les techniques de ''vertex texturing'', mais de vraies lectures arbitraires, pour lire les tampons de sommet/indice. Cette possibilité est arrivée avec Direct X 10, ce qui fait que l’''input assembler'' peut être émulé par les ''vertex shaders'' à partir de cette version de Direct X. De nos jours, tous les GPUs font à leur sauce. Certains émulent l’''input assembler'' avec des ''shaders'', d'autres non. Ceux qui le font le font en modifiant les ''vertex shaders''. Le ''driver'' du GPU injecte du code dans les ''vertex shaders'', code qui émule l'''input assembler''. ===Les caches de sommets : une optimisation du tampon d'indice=== Idéalement, le ''vertex shader'' doit être exécuté une seule fois par sommet (idem pour son équivalent avec une unité de T&L). Mais quand des sommets sont dupliqués, ce n'est pas le cas. Le problème se comprend bien si on prend une représentation non-compressée, où les sommets sont dupliqués si nécessaires. Le résultat est que les copies d'un même sommet sont toutes lues depuis la mémoire, transformées, éclairées, puis envoyées à l'unité d'assemblage de primitives. En clair : un sommet est lu en VRAM plusieurs fois, et subit des calculs géométriques redondants. Ce qui est un problème. Les représentations compressées permettent de grandement réduire cette redondance. Les ''triangle strip'' et ''triangle fan'' sont de loin les plus efficaces, de ce point de vue : un sommet n'est chargé qu'une seule fois, et n'est traité qu'une seule fois. Du moins, si tout se passe bien. En effet, pour convertir un modèle 3D en ''triangle strip/fan'', il faut parfois ruser, ce qui fait que des sommets sont redondants. Avec la représentation indicée, l'''input assembler'' doit détecter quand un sommet dupliqué a déjà été rencontré. Si un tel sommet dupliqué est détecté, on récupère le sommet déjà calculé, plutôt que de refaire les calculs. Mais cela demande d'ajouter une mémoire cache pour mémoriser les sommets transformés/éclairés. Elle est appelée le '''''Post Transform Cache''''' et il est crucial pour éviter les calculs redondants. L'idée est la suivante : en sortie de l’''index fetch'', un circuit regarde les indices chargés et vérifie s'ils ont déjà été rencontrés. Si l'indice est inconnu, alors on suppose que le sommet associé n'a jamais été rencontré. L'indice est envoyé à l'unité de ''vertex fetch'', le sommet est chargé depuis le tampon de sommet et envoyé à l'unité géométrique. Par contre, si l'indice est reconnu, c'est que le sommet associé a déjà été transformé/éclairé : on lit alors le sommet transformé depuis le ''Post Transform Cache''. Pour détecter un sommet déjà rencontré, rien de plus simple : il suffit de consulter le ''Post Transform Cache''. Une fois un indice chargé, le ''Post Transform Cache'' est consulté pour vérifier s'il a une copie du sommet associé. Le cache répond alors soit en disant qu'il n'a pas le sommet associé, soit il renvoie le sommet transformé. Le ''Post Transform Cache'' est consulté en lui envoyant l'indice du sommet, et potentiellement de quoi identifier le tampon d'indice utilisé. C'est pour ne pas confondre deux sommets appartenant à deux modèles différents mais qui ont le même indice par hasard. Deux solutions pour cela : soit on utilise un identifiant pour le tampon d'indice utilisé (pas une adresse), soit on vide le cache entre deux ''draw call''. Il est vraisemblable que tout soit plus compliqué. En, effet, il faut tenir compte du cas où un sommet est en cours de calcul. Pour gérer ce cas, il est probable que l’''input assembler'' réserve de la place dans ce cache à l'avance. Quand un sommet est envoyé aux unités géométriques, l’''input assembler'' doit réserver de la place dans le cache, en mettant l'indice dans le ''tag'' du cache, et en laissant la ligne de cache vide. Le ''Post Transform Cache'' mémorise les N derniers sommets rencontrés. Elle est souvent qualifiée de mémoire FIFO, mais c'est un intermédiaire entre une mémoire cache du point de vue des lectures, et une mémoire FIFO du point de vue des écritures. Il mémorise entre 16 et 64 sommets, pas plus. Aller au-delà ne sert pas à grand chose, vu que des sommets dupliqués sont très souvent proches en mémoire RAM et sont traités dans une fenêtre temporelle assez petite. [[File:Post-transform cache.png|centre|vignette|upright=2|Post-transform cache]] Le ''Post-transform cache'' se trouve donc en sortie de l'unité d’''index fetch''. Mais serait-il possible d'ajouter un second cache, cette fois-ci pour l'unité de ''vertex fetch'' ? Un tel cache existe lui aussi, et s’appelle le '''''pre-transform cache'''''. Il mémorise les sommets chargés, mais pas encore transformés/éclairés. Il se situe entre l'unité de ''vertex fetch'' et l'unité géométrique. Intuitivement, on se dit qu'il évite de charger un sommet plusieurs fois. Mais ce n'est en réalité qu'un intérêt secondaire, bon à prendre, mais pas primordial. En réalité, il permet de profiter du fait que le ''vertex fetch'' charge les sommets par paquets de 32 à 64 sommets, qui sont copiés dans le cache de sommets. Ainsi, quand on charge un sommet, les 32/64 suivants sont chargés avec et sont disponibles pour l'unité de ''vertex shader'' si celle-ci en a besoin dans le futur, ce qui a de très fortes chances d'être le cas. De plus, il est possible de précharger des lignes de cache : quand le ''vertex fetch'' lit un paquet de sommets, le paquet de sommet est copié dans le cache, mais les paquets suivants peuvent aussi être chargés en avance. Une telle technique de '''préchargement'' permet d'améliorer les performances. [[File:Pre- et Post-transform cache.png|centre|vignette|upright=2|Pre- et Post-transform cache]] Pour résumer, l’''input assembler'' contient deux caches, qui sont collectivement appelés des '''caches de sommets'''. Le ''Post Transform Cache'' a disparu dans certains GPU modernes. Je recommande la lecture de l'article "Revisiting The Vertex Cache : Understanding and Optimizing Vertex Processing on the modern GPU" à ce sujet. Quant au ''Pre Transform Cache'', il a été remplacé par des mémoires caches généralistes, qui ne sont pas spécialisées dans les sommets. ===L'assemblage de primitives=== En sortie des unités géométriques, on a des sommets éclairés et colorisés, pas des triangles. Pour recréer des triangles, on doit lire les sommets dans l'ordre adéquat, par paquets de trois pour obtenir des triangles. C'est le rôle de l''''étape d'assemblage de primitives''' (''primitive assembly''), qui regroupe les sommets appartenant au même triangle, à la même primitive. L'assemblage des primitives est réalisée par un circuit fixe, non-programmable, qui utilise le tampon d'indice pour regrouper les sommets en primitives. Un problème pour l'assemblage de primitives est que les sommets n’arrivent pas dans l'ordre. Il arrive que des sommets soit traités plus vite que les autres, et passent devant. Le pipeline ne peut pas se baser sur l'ordre d'arrivée des sommets, pour regrouper les sommets en triangles. Pour gérer ces temps de calcul variable, le pipeline mémorise les triangles en sortie des unités géométriques et attend que tous les sommets d'un triangles soient disponibles. La méthode pour cela dépend de la représentation utilisée. L'assemblage des primitives ne se passe pas pareil avec les ''triangle strip'', ''triangle fan'', représentation indicée et représentation non-compressées. Avec la représentation non-compressée, l'assemblage de primitives regroupe les triangles par paquets de trois, rien de plus. Mais attention, des triangles consécutifs en mémoire ne sortent pas des unités géométriques l'un à la suite de l'autre. Pour gérer ça, l'''input assembler'' associe, un numéro à chaque triangle, qui indique sa place dans le tampon de sommets, qui est un indice. L'assemblage de primitive regarde ces numéros pour regrouper les triangles. Il attend que trois numéros consécutifs soient disponibles pour assembler le prochain triangle. Pour l'adressage indicé, il procède comme la représentation non-compréssée, sauf qu'il regarde le tampon d'indice. Il lit le tampon d'indice en partant du début, et fait des groupes de trois indices consécutifs. Les sommets sont associés avec leur indice, qui les accompagne lors de leur trajet dans le pipeline géométrique. Une fois qu'ils sortent des unités géométriques, ils sont accumulés dans une mémoire juste avant l'unité de primitive, et l'assemblage de primitive attend que les trois sommets avec les trois indices adéquats soient disponibles. Avec les ''triangle strip'', il mémorise les deux derniers sommets chargés, pour les combiner avec le prochain sommet à charger. L'implémentation matérielle est assez simple : un registre pour mémoriser le premier sommet, une mémoire FIFO pour mémoriser les deux sommets les plus récents. Pour générer un triangle, l'étape d'assemblage de primitive lit le registre et la mémoire FIFO, pour récupérer les trois sommets. Avec les ''triangle fan'', il doit mémoriser le sommet partagé, et le dernier sommet chargé, ce qui demande deux registres. ==Les ''geometry shaders''== Les GPU d'avant DirectX 10, qui n'avaient que les ''vertex shaders'' et ne pouvaient manipuler que des sommets. Depuis DirectX 10, le pipeline graphique a intégré des techniques pour gérer nativement des triangles dans les ''shaders''. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. L'intérêt est que cela permet de faciliter l'implémentation de techniques de tesselation, sans compter que certaines optimisations deviennent plus simples à effectuer. Dans ce chapitre, nous allons étudier le pipeline graphique de DirectX 10, DirectX 11 et DirectX 12. DirectX 10 et OpenGl 3.2 ont introduit les ''geometry shaders'', juste avant l'étape d'assemblage des primitives. Les ''geometry shaders'' peuvent ajouter, supprimer ou altérer des primitives dans une scène 3D. Un ''geometry shader'' prend en entrée un point, une ligne ou un triangle, donc les trois primitives de base supportées sur les GPU modernes. Il émet en sortie : soit un ''triangle strip'', soit une ''line strip'' (c'est à une ligne ce qu'un d'un ''triangle strip'' est à un triangle) ou un point. Ils n'ont pas été très utilisés, leurs utilisations étant assez limitées. Ils peuvent en théorie être utilisés pour la gestion des ''cubemaps'', le ''shadow volume extrusion'', la génération de particules, et quelques autres effets graphiques. Ils pourraient aussi être utilisés pour faire de la tesselation, mais leurs limitations font que ce n'est pas pratique. Rappelons que les ''geometry shaders'' sont optionnels et que beaucoup de jeux vidéos ou de moteurs de rendu 3D n'en utilisent pas. ===La conservation de l'ordre des sommets entrants et sortants=== Les ''geometry shaders'' sont exécutés après l'assemblage de primitive, car ils manipulent les primitives fournies par l'étape d'assemblage des primitives. Les ''geometry shaders'' n'ont jamais eu de processeur de shader dédié, car ils ont été introduits avec DirectX 10 et OpenGl 3.2, en même temps que les processeurs de ''shaders'' ont étés unifiés (rendu capable d’exécuter n'importe quel ''shader''). Leur place dans le pipeline graphique est quelque peu étrange. Un point important est que DirectX 10 impose de conserver l'ordre d'envoi des sommets. Si les sommets arrivent dans un certain ordre, il ressortent du ''geometry shader'' dans ce même ordre. Faire ainsi simplifie grandement les choses pour le programmeur. Mais cela impose des contraintes pour le GPU. Les sommets ont beau être envoyés dans l'ordre aux processeurs, certains peuvent être traités plus vite que les autres. Et quand on distribue des sommets sur pleins de processeurs de shader, cela fait que l'ordre de sortie change. Pour corriger cela, les sommets sortants du ''geometry shader'' doivent être remis en ordre. Une première solution est de les mettre en attente dans un second tampon de primitives, pour les remettre en ordre avant la rastérisation. Les primitives sortent des ''geometry shaders'' dans le désordre, sont ajoutées dans le tampon de primitive dans le désordre, mais la rastérisation les consomme dans l'ordre. [[File:Implémentation matérielle des geometry shaders.png|centre|vignette|upright=2|Implémentation matérielle des geometry shaders]] Au passage, j'ai menti plus haut en disant que les ''geometry shaders'' fournissent en entrée de 0 à plusieurs primitives : la sortie d'un ''geometry shader'' est un ensemble de sommets, souvent complété par un mini-tampon d'indice indiquant comment assembler ces sommets en primitives. Le résultat est que l'assembleur de primitive doit refaire son travail après le passage d'un ''geometry shader''. Heureusement, la sortie d'un ''geometry shader'' est soit un point, soit une ligne, soit un ''triangle strip'', ce qui simplifie la seconde phase d'assemblage des primitives. Avec les ''geometry shaders'', il y a donc deux phases d'assemblage des primitives : une phase avant, décrite dans la section précédente, et une seconde phase simplifiée après les ''geometry shaders''. Il n'y a pas que la phase d'assemblage de primitives qui est dupliquée : le tampon de primitives l'est aussi. On trouve donc un tampon de primitives à l'entrée des ''geometry shaders'' et un autre à la sortie. ===Les complications liées à la sortie des ''geometry shaders''=== J'ai dit plus haut que le GPu incorpore un second tampon de primitives. Mais sur quelques GPU, les résultats d'un ''geometry shader'' ne passent pas directement par un second tampon de primitives. A la place, ils sont mémorisés en mémoire vidéo, avant d'être lu par l'assemblage de primitives. C'était très lent, mais c'est nécessaire pour une raison qu'on va expliquer immédiatement. Un ''geometry shader'' fournit un résultat très variable en fonction de ses entrées. Pour une même entrée, la sortie peut aller d'une simple primitive à plusieurs dizaines. Le ''geometry shader'' précise cependant un nombre limite de sommets qu'il ne peut pas dépasser en sortie. Il peut ainsi préciser qu'il ne sortira pas plus de 16 sommets, par exemple. Et ce nombre maximal est celui qui est utilisé pour savoir comment organiser le tampon de primitive. Par exemple, si jamais on a un tampon de primitive capable de mémoriser 1024 sommets, celui-ci peut être partitionné en 512 blocs de deux sommets, ou 256 blocs de 4 sommets, 128 blocs de 4 sommets, etc. Pour savoir comment subdiviser le tampon de primitives en parts égales, il n'y a qu'une seule solution : diviser le tampon de primitive par des blocs de taille maximale. Ainsi, si le shader dit qu'il aura en sortie entre 0 et 16 sommets maximum, on doit diviser le tampon en parts de 16 sommets, ce qui fait maximum 1024/16 = 128 instances de shaders maximum. En conséquence, le second tampon de primitives sera sous-utilisé en pratique. Et le principe reste le même si on change les chiffres exacts : chaque instance de shader reçoit une certaine portion du tampon de primitive, égale à la taille du tampon de primitives divisée par ce nombre limite. Vous noterez que la répartition n'est pas dynamique, mais statique. C'est la méthode la plus simple niveau matériel et celle qui coute le moins en circuits, malgré sa mauvaise utilisation, du tampon de primitives. Le problème est que le nombre d'instances exécutables en parallèle est rapidement limité. Une solution à cela est la suivante. Quand un ''geometry shader'' a terminé son travail, il regarde s'il y a de la place dans le second tampon de primitives. Si celui-ci est plein, il attend que de la place se libère. On a donc un processeur de shader qui ne fait rien. les primitives calculées sont juste mémorisées dans les registres en attendant d'être transférées au tampon de primitives. Au pire, on peut espérer qu'une autre instance s'exécute dans un autre ''thread'', grâce aux propriétés de ''multithreading'' matériel. Le nombre de ''geometry shader'' pouvant attendre est alors limité par le nombre de registres du processeur, et la taille des ''shaders''. Avoir beaucoup de registres est alors un avantage ([http://www.joshbarczak.com/blog/?p=667 Why Geometry Shaders Are Slow (Unless you’re Intel)]). Une solution alternative est de mémoriser le résultat des ''geometry shaders'' en mémoire RAM, pour ensuite relire le résultat pour l'envoyer à la rastérisation. Pas besoin de second tampon de primitives, les limitations de nombre de shaders exécutés en parallèle disparaissent. Les processeurs de shaders sont utilisés au maximum, mais le cout en bande passante mémoire est assez élevé. les performances ne sont donc pas franchement meilleures. : Il n'y a pas le même problème avec les ''vertex shaders'' car ils ne font que modifier des sommets : pour N sommets en entrées, ils fourniront N sommets en sortie. Ainsi, si on X processeurs de shaders pouvant traiter Y sommets en même temps avec leurs instructions SIMD, on peut prévoir le nombre de sommets en sortie. Le tampon de primitive est conçu pour encaisser ce nombre de sommets sortants, voire beaucoup plus. Il est rarement un point bloquant en termes de performances. ==Les ''mesh shaders''== [[File:D3D11 Pipeline.svg|vignette|upright=1|Pipeline graphique de Direct x 11.]] Avec l'introduction des ''geometry shaders'' et de la tesselation, le pipeline graphique est devenu très complexe. Plusieurs étages en plus sont ajoutés à sa portion géométrique : un pour les ''geometry shaders'', trois pour la tesselation, et ce en plus des ''vertex shaders'' existants et des étages non-programmables. Le pipeline en question est celui d'Open GL 4 et de DirectX 11. Mais Direct X 12 a simplifié le tout, sous l'impulsion de technologies introduites par AMD et de NVIDIA. AMD a introduit les ''primitive shaders'', NVIDIA a introduit les ''mesh shaders'''' ont été introduit par NVIDIA. Les derniers ont été gardés pour DirectX 12, simplifiant grandement le pipeline. ===Les primitive/mesh shaders=== Les deux solutions de AMD et NVIDIA partent du même principe : elles fusionnent certaines étapes du pipeline. Les ''primitive/mesh shaders'' font disparaitre les étapes d{{'}}''input assembly'' et d'assemblage de primitives, qui sont maintenant gérées par les ''primitive/mesh shaders''. Les ''primitive/mesh shaders'' lisent directement le tampon d'indice et lisent les sommets depuis la VRAM, sans passer par une étape non-programmable. Ils assemblent les primitives eux-mêmes et les envoient directement au rastériseur. Le tout permet des optimisations très intéressantes, comme un ''culling'' précoce. Les ''mesh shaders'' sont des ''shaders'' généralistes, semblables aux ''compute shaders''. Pour rappel, un ''compute shader'' peut lire des données en RAM, exécuter des traitements dessus, et enregistrer les résultats en RAM. Il peut lire ou écrire à des adresses arbitraires, sans limitations. Il n'est pas limité à lire des données consécutives, peut sauter d'une donnée à une autre donnée distante en RAM. Les ''mesh shaders'' sont des variantes des ''compute shaders'', qui n'écrivent pas leur résultat en RAM, mais envoient celui-ci au rastériseur. Plus précisément, ils écrivent leur résultat dans le tampon de primitives. Les ''mesh shaders'' peuvent contourner l'étape d{{'}}''input assembly'' et la remplacer par leur propre code. Pour rappel, l'étape d{{'}}''input assembly'' était non-programmable et gérait des tampons de vertices et d'indices très normés. Les sommets étaient lus soit un par un, soit par paquets de N sommets consécutifs, ce qui était assez rigide. Il n'y avait pas d'accès arbitraire en mémoire RAM comme peuvent le faire les ''compute shaders''. Par contre, un ''mesh shader'' peut accéder aux sommets de la manière qu'il souhaite, ce qui permet d'émuler un ''input assembler'' normal et plus encore. Une autre différence avec les ''vertex shaders'' est qu'ils ne traitent pas forcément des sommets, mais peuvent aussi envoyer des primitives au rastériseur directement. En clair, ils n'ont pas besoin d'une étape de ''primitive assembly'', qu'ils peuvent émuler directement dans le ''shader'' lui-même. Le ''culling'' est lui aussi réalisé par le ''primitive shader'', pas par une unité fixe. Et cela permet de contourner un problème fondamental des ''vertex shaders'' : il fallait que les primitives soient assemblées pour qu'on puisse déterminer si elles sont ou non invisibles. A l'opposé, les ''primitive/mesh shaders'' assemblent les primitives de manière précoce dans le ''primitive/mesh shader'', ce qui permet d'éliminer les primitives invisibles le plus tôt possible. Pour cela, les opérations permettant de déterminer si une primitive est visible sont exécutés en priorité, les autres opérations sont retardées et effectuées le plus tard possible. Ainsi, les calculs pour colorier ou orienter un sommet ne sont pas exécutés si le sommet est invisible. Il y a des différences entre ''primitive'' et ''mesh shaders''. Les ''primitive shaders'' permettent de lire un sommet à la fois, alors que les ''mesh shaders'' permettent de lire des ''batchs'' de plusieurs primitives d'un coup. Ces ''batchs'' de plusieurs primitives sont appelés des meshlets. La différence n'est pas fondamentale : le hardware des cartes AMD, qui gère des ''primitive shaders'', peut regrouper dynamiquement plusieurs instances de ''primitive shaders'' en un seul ''mesh shader'', via les technique de SIMT (une instance de ''primitive shader'' effectue des opérations scalaires, qui peuvent être regroupées en une seule instance SIMD en traitant plusieurs sommets en parallèle). La seule différence est que les ''mesh shaders'' exposent ce comportement au niveau du jeu d'instruction des ''shaders'', les programmeurs en ont conscience. ===Le pipeline géométrique avec les ''primitive/mesh shaders''=== Avec les ''primitive shaders'', l'implémentation exacte dépend de si la tesselation est activée ou non. Si la tesselation n'est pas activée, le ''vertex shader'' et le ''geométry shader'' sont fusionnés en un seul ''primitive shader''. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, sans tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="4" | |- ! DirectX 12 | colspan="4" | ''Primitive shader'' (AMD) |} Avec la tesselation activée, les ''geometry shaders'' et les ''domain shaders'' en un seul ''shader''. De même, les ''vertex shaders'' et les ''hull shaders'' sont fusionnés en un seul ''shader'', nommé l{{'}}''amplification shader''. Ainsi, le pipeline graphique est grandement simplifié, avec seulement deux ''shaders'' et un étage fixe, au lieu de quatre ''shaders'' différents. {|class="wikitable" |+ Comparaison entre les pipelines géométriques de DirectX 11 et 12, avec tesselation |- ! DirectX 11 | class="f_rouge" | ''Input assembly'' | ''Vertex shader'' | ''Hull shader'' | class="f_rouge" | Tesselation | ''Domain shader'' | ''Geometry shader'' | class="f_rouge" | ''Primitive assembly'' |- | colspan="7" | |- ! DirectX 12 | colspan="3" | * ''Amplification shader'' (AMD) | class="f_rouge" | Tesselation | colspan="3" | * ''Primitive shader'' (AMD) |} <noinclude> {{NavChapitre | book=Les cartes graphiques | prev=Le pipeline géométrique : évolution | prevText=Le pipeline géométrique : évolution | next=Le rasterizeur | nextText=Le rasterizeur }}{{autocat}} </noinclude> l9txvyceh19tu8nvz3eijbasoyn859m Le mouvement Wikimédia/L'utopie Wikimédia 0 79253 763851 757099 2026-04-17T06:03:56Z Lionel Scheepmans 20012 763851 wikitext text/x-wiki <noinclude>{{Le mouvement Wikimédia}}</noinclude> Pour mieux comprendre pourquoi Wikipédia et le mouvement Wikimédia sont perçus comme une utopie et même par certain comme « la dernière utopie collective du Web »<ref>{{Lien web|langue=fr-FR|nom1=Dupont-Besnard|prénom1=Marcus|titre=Wikipédia, la dernière utopie collective du web ?|url=https://web.archive.org/web/20250516050814/https://www.numerama.com/tech/805447-wikipedia-la-derniere-utopie-collective-du-web.html|site=Numerama|date=2021-12-29|consulté le=2025-12-21}}</ref>, voici une métaphore qui aide a comprendre les activités en ligne du mouvement Wikimédia, sur base d'un espace web imaginé tel une gigantesque ville numérique. Une ville constituée d'un réseau routier, qui représente Internet et d'un ensemble de batiments, qui représentent les serveurs informatiques hébergeant les millions de sites présents dans l'espace web. Dans cette ville numérique se trouve alors un immense quartier, le quartier Wikimédia, où se rassemblent près d’un millier d’édifices que l’on peut visiter librement et gratuitement. A l’exception de quelques bâtiments administratifs, non seulement on peut explorer tout cela librement et gratuitement, mais en plus, on peut aussi y modifier presque tout ce qui s’y trouve. On peut y apporter de nouvelles informations, sous forme de texte, photo, vidéo et document sonore, ou encore, si l’on le veut, ranger les informations apportées par d’autres personnes, afin de rendre leurs présentations plus esthétiques ou plus compréhensibles. D’une manière plus incroyable encore, on peut même faire disparaitre l’entièreté de ce qui est présent dans une pièce. Suite à quoi, un programme informatique remettra tout en place, avant de demander gentiment d’éviter ce genre de vandalisme. Concernant les actions plus discrètes et non détectées par les robots, l'une des personnes qui ont enrichi ou enjolivé la pièce, viendra certainement annuler les modifications malveillantes avant de contacter l'auteur. Et si c'est un multirécidiviste, il sera alors privé du droit de modification du bâtiment qu'il a vandalisé, et même dans tout le quartier si cela se justifie. Une décision qui, par ailleurs, sera toujours mise en application par un des volontaires administrateurs choisis par la communauté des bénévoles actifs au sein des projets. [[Fichier:The Digital City, Riyadh 191957.jpg|vignette|<small>Figure 3. Photo de la ''Digital City'' de [[w:Riyad|Riyadh]] et son aspect visuel en lien avec la métaphore du quartier Wikimédia.</small>|300x300px]] On comprend donc que tout le monde peut enrichir, mais aussi surveiller et protéger les richesses partagées dans le quartier Wikimédia. Il suffit pour cela de rejoindre le mouvement en se créant un compte et de profiter, entre autres, d'un système de notification qui poste un avertissement à chaque fois qu'une des pièces que l'on désire surveiller au sein des bâtiments Wikimédia est modifiée. Pour la création de ce compte, pas besoin de fournir une adresse ou un numéro de téléphone. Les seules informations personnelles indispensables au bon fonctionnement du quartier Wikimédia sont les [[w:Adresses_IP|adresses IP]] des ordinateurs connectés. Car contrairement à ce qui se passe dans les quartiers commerciaux de la grande ville numérique, tels que les [[w:GAFAM|GAFAM]], [[w:NATU_(Netflix,_Airbnb,_Tesla_et_Uber)|NATU]], [[w:BATX|BATX]] ou autres, aucune des informations récoltées lors des visites des bâtiments Wikimédia n’est exploitée par des services de marketing. Même les adresses IP enregistrées par le système ne sont pas visibles par les autres visiteurs. Elles sont remplacées par les noms et les pseudonymes fournis lors de la création des comptes, ou masquées par des comptes temporaires en cas de non-connexion. Seules quelques personnes accréditées par la communauté pour effectuer des contrôles d’usurpation d’identité ont accès à ces informations<ref>{{Lien web|auteur=MédiaWiki|titre=Produit de confiance et de sécurité/Comptes temporaires|url=https://web.archive.org/web/20250813140004/https://www.mediawiki.org/wiki/Trust_and_Safety_Product/Temporary_Accounts/fr}}.</ref>. C’est là une précaution nécessaire au bon déroulement des prises de décisions par recherche de [[w:Consensus|consensus]] organisées au sein du quartier Wikimédia. Dans la ville numérique que constitue le Web, Wikimédia apparait ainsi comme le plus grand quartier dédié au partage de la connaissance. La partie la plus connue du quartier, [[w:Wikipédia:Accueil_principal|Wikipédia]], est composée de plus de 350 bâtiments encyclopédiques répertoriés par langues. À côté de ceux-ci, et toujours séparés en versions linguistiques, se trouvent les bibliothèques [[:en:fr:accueil|Wikilivres]] et [[s:fr:Wikisource:Accueil|Wikisource]], les bâtiments lexicaux multilingues [[wikt:fr:Wiktionnaire:Page_d’accueil|Wiktionnaire]], ce centre journalistique [[n:fr:accueil|Wikinews]], le centre pédagogique et de recherche [[v:fr:accueil|Wikiversité]], le centre d'informations sur les voyages [[voy:fr:accueil|Wikivoyage]], le répertoire des êtres vivants [[species:main page|Wikispecies]] et l'institut des citations d’auteurs [[q:fr:accueil|Wikiquote]]. Cela sans oublier [[commons:main page|Wikimedia Commons]], un lieu de collecte pour tous les fichiers médiatiques, et [[wikidata:wikidata:main_page|Wikidata]], cette énorme banque d’informations structurées, dont la fonction, au même titre que Wikimedi Commons, est d’enrichir le contenu des autres buildings. 50 % de ces bâtiments sont constitués d'étages dédiés à la libre organisation technique et politique des projets. Tandis que plusieurs immeubles du quartier, tels que [[mw:main page|MediaWiki]], [[wikitech:Main_Page|Wikitech]], [[w:fr:phabricator|Phabricator]], sont entièrement dédiés à sa maintenance technique. Vient ensuite [[metawiki:main page|Méta-Wiki]], le centre administratif dédié à l’organisation et la gouvernance générale de l'ensemble du quartier. Puis le bâtiment [[otrswiki:Main page|Wikimedia VRT]], où l'on traite les courriers adressés au quartier, et finalement [[outreach:main page|''Wikimedia outreach'']], le centre de sensibilisation et de recrutement de nouveaux bénévoles. En découvrant l’existence de ce vaste quartier numérique libre d’accès, de participation et modification, on visualise mieux, à nouveau, comment les activités du mouvement Wikimédia dépassent largement ce qui se passe au sein du projet Wikipédia. Même si à lui seul, ce projet est déjà perçu comme « une utopie en marche »<ref>{{Article|langue=|prénom1=Christian|nom1=Vandendorpe|titre=Le phénomène Wikipédia: une utopie en marche|périodique=Le Débat|volume=148|numéro=1|éditeur=Gallimard|date=2008|issn=0246-2346|pages=17}}.</ref> ou « réalisée »<ref>{{Ouvrage|prénom1=Théo|nom1=Henri|directeur1=|titre=Wikipédia : une utopie réalisée ?|lieu=Université de Poitier|date=juillet 2013|pages totales=98|lire en ligne=https://web.archive.org/web/20211103122912/https://www.seies.net/sites/theo/doc/HENRI_theo_-_master_1_-_memoire.pdf}}.</ref>, il fait cependant partie d'une organisation mondiale bien plus vaste, quasi gérée uniquement par des bénévoles et de manière non lucrative. Une utopie bien plus grande donc, dont il est bon de comprendre les origines. Comme première explication, il y a cette synchronicité entre le chamboulement culturel provoqué par la [[w:Contre-culture_des_années_1960|contre-culture des années 1960]] et les débuts de la révolution numérique. Les chercheurs et étudiants en informatique, pionniers des réseaux et de leurs applications, étaient effectivement fortement influencés par ce changement de paradigme, qui fut symbolisé en France par les événements de [[w:mai_68|mai 68]]. De cette impulsion naîtront ainsi une philosophie et un mode d’organisation des activités tout à fait spécifiques, dont le mouvement Wikimédia deviendra l'héritier. Aujourd’hui, et à l'image du projet Wikipédia en français<ref>{{Lien web|auteur=Wikipédia|titre=Wikipédia:Règles et recommandations|url=https://web.archive.org/web/20251008105309/https://fr.wikipedia.org/wiki/Wikip%C3%A9dia:R%C3%A8gles_et_recommandations|date=|consulté le=}}.</ref>, les sites web hébergés par la Fondation Wikimédia contiennent de nombreuses [[w:Wikipédia:Règles_et_recommandations|règles et recommandations]] qui s'apparentent fortement à des lignes éditoriales. Parallèlement à cela, un [[foundation:Policy:Universal_Code_of_Conduct/fr|code de conduite universel]] fut adopté par la Fondation Wikimédia en 2022, dans le but d’établir un « référentiel minimum des comportements acceptables et inacceptables »<ref>{{Lien web|titre=Policy:Universal Code of Conduct/fr|url=https://web.archive.org/web/20251007061014/https://foundation.wikimedia.org/wiki/Policy:Universal_Code_of_Conduct/fr|site=|date=|consulté le=|auteur=Wikimedia Foundation Governance Wiki}}.</ref>. Cependant, dès la création de l'encyclopédie libre qui fut un jour qualifiée de « bazar libertaire »<ref>{{Lien web|langue=|auteur=Frédéric Joignot|titre=Wikipédia, bazar libertaire|url=https://web.archive.org/web/20170630065818/http://www.lemonde.fr/technologies/article/2012/01/14/wikipedia-bazar-libertaire_1629135_651865.html|site=Le Monde|lieu=|date=2012|consulté le=}}.</ref>, un principe fondateur intitulé [[w:Wikipédia:Interprétation_créative_des_règles|interprétation créative des règles]]<ref>{{Lien web|langue=|auteur=Wikipédia|titre=Wikipédia: Interprétation créative des règles|url=https://fr.wikipedia.org/wiki/Wikip&#233;dia:Interpr&#233;tation_cr&#233;ative_des_r&#232;gles|consulté le=}}.</ref>, toujours présent à ce jour, illustre clairement l’orientation politique et culturelle du projet : <blockquote> N’hésitez pas à contribuer, même si vous ne connaissez pas l’ensemble des règles, et si vous en rencontrez une qui, dans votre situation, semble gêner à l’élaboration de l’encyclopédie, ignorez-la ou, mieux, corrigez-la. </blockquote> Cette invitation illustre à elle seule les valeurs d’universalité, de liberté, de décentralisation, de partage, de collaboration et de mérite décrites par [[w:fr: Steven Levy|Steven Levy]] dans son ouvrage ''[[w:L'Éthique des hackers|L’Éthique des hackers]]''<ref>{{Ouvrage|langue=|prénom1=Steven|nom1=Levy|prénom2=Gilles|nom2=Tordjman|titre=L'éthique des hackers|éditeur=Globe|date=2013|isbn=978-2-211-20410-1|oclc=844898302}}.</ref>. Bien avant l’apparition du mouvement Wikimédia, une nouvelle perception du monde a donc rendu possible le développement d'un environnement numérique, ouvert, libre et gratuit, propice à la création d'une encyclopédie, écrite et gérée par des millions de contributeurs et contributrices situés aux quatre coins du monde. Cela commence par [[w:Internet|Internet]], un réseau mondial de communication en libre accès, puis le développement du ''[[w:World_Wide_Web|World Wide Web]]'', qui simplifie les interactions humaines à l’échelle planétaire, jusqu'à l'avènement des logiciels qui ont rendu possible la modification de sites web à l'aide d’un simple navigateur, pour former ce que l'on appelle aujourd’hui le [[w:Web_2.0|Web 2.0]]. Or, parmi ces logiciels se trouvent les [[w:fr:Moteur de Wiki|moteurs de Wiki]], dont le plus puissant d’entre eux, [[w:MediaWiki|MediaWiki]], est un [[Logiciels libres|logiciel libre]] développé par la Fondation Wikimédia, devenue par ce fait actrice au sein du [[w:Mouvement_du_logiciel_libre|mouvement du logiciel libre]].{{AutoCat}} kvhxdxgxxq623ktwqe69z12o6w6dql2 Les cartes graphiques/La répartition du travail sur les unités de shaders 0 79263 763817 763460 2026-04-16T20:30:03Z Mewtow 31375 /* La répartition du travail pour le rendu graphique */ 763817 wikitext text/x-wiki Un GPU plusieurs processeurs de shaders, chacun traitant plusieurs sommets/pixels à la fois. La répartition du travail sur plusieurs processeurs de ''shaders'' est un vrai défi sur les cartes graphiques actuelles. La répartition du travail sur plusieurs processeurs de ''shaders'' est le fait du '''processeur de commande''', un circuit de la carte graphique. Ce n'est pas son seul rôle, mais c'est clairement une fonctionnalité très importante que prend en charge le processeur de commande. ==La répartition du travail pour le GPGPU== Avant de voir ce qu'il en est pour le rendu 3D, nous allons faire un détour par les fonctionnalités dites de GPGPU. Outre le rendu 3D, les cartes graphiques modernes sont utilisées pour accélérer des calculs scientifiques, tout ce qui implique des réseaux de neurones, de l'imagerie médicale, etc. De manière générale, tout calcul faisant usage d'un grand nombre de calculs sur des matrices ou des vecteurs est concerné.. L'usage d'une carte graphique pour autre chose que le rendu 3D porte le nom de '''GPGPU''' (''General Processing GPU''). En soi, le GPGPU est assez logique : les processeurs de shaders, bien que conçus avec le rendu 3D en tête, n'en restent pas moins des processeurs multicœurs SIMD/VLIW assez puissants. Si nous voyons le GPGPU avant le rendu 3D, c'est pour une raison simple : la répartition du travail sur les processeurs de shaders est alors nettement plus simple. En GPGPU, les shaders font des calculs génériques, à savoir qu'ils ne travaillent pas sur des pixels ou des vertices. Ils n'ont donc pas à communiquer avec le rastériseur ou l'''input assembler'', ils ne lisent même pas de textures dans la RAM. Les processeurs de shaders communiquent seulement avec la mémoire vidéo, mais pas avec le moindre circuit fixe. La répartition du travail en GPGPU est donc beaucoup plus simple qu'en mode graphique, le processeur de commande a nettement moins de travail. ===La répartition du travail en GPGPU n'est pas celle du mode graphique=== Du point de vue du GPGPU, l'architecture d'une carte graphique récente est illustrée ci-dessous. Les processeurs/cœurs sont les rectangles en bleu/rouge, le bleu et le rouge correspondant à des circuits de calcul différents. La hiérarchie mémoire est indiquée en vert. Le tout est alimenté par un processeur de commande, en jaune, ici appelé le ''Thread Execution Control Unit''. [[File:NVIDIA GPU Accelerator Block Diagram.png|centre|vignette|upright=2.5|Ce schéma illustre l'architecture d'un GPU en utilisant la terminologie NVIDIA. Comme on le voit, la carte graphique contient plusieurs cœurs de processeur distincts, ayant chacun plusieurs unités de calcul appelées malencontreusement "processeurs de threads". Ces cœurs sont alimentés en instructions par le processeur de commandes, ici appelé ''Thread Execution Control Unit'', qui répartit les différents shaders sur chaque cœur. Enfin, on voit que chaque cœur a accès à une mémoire locale dédiée, en plus d'une mémoire vidéo partagée entre tous les cœurs.]] En GPGPU, un ''shader'' s'exécute sur des regroupements de données bien connus des programmeurs : des tableaux. Pour rappel, un tableau est un ensemble d'entiers ou de flottants qui sont consécutifs en mémoire RAM. Les tableaux peuvent être de simples tableaux, des matrices, peu importe. Le processeur envoie à la carte graphique un ''shader'' à exécuter et les tableaux à manipuler. Les tableaux ont une taille variable, mais sont presque toujours de très grande taille, au moins un millier d’éléments, parfois un bon million, si ce n'est plus. Le processus de répartition du travail est globalement le suivant. Le processeur de commande reçoit une '''commande de calcul GPGPU''', qui précise quel ''shader'' exécuter, et fournit l'adresse de plusieurs tableaux, ainsi que des informations sur le format des données (entières, flottantes, tableau en une ou deux dimensions, autres). Le processeur de commande découpe les tableaux en vecteurs de taille fixe, qu'un processeur de shader peut gérer. Par exemple, si un processeur SIMD gère des vecteurs de 32 entiers/flottants, alors le tableau est découpé en morceaux de 32 entiers/flottants, et chaque processeur exécute une instance du ''shader'' sur des morceaux de cette taille. Cependant, il faut tenir compte que les processeurs de shader sont multithréadés. Ils peuvent gérer plusieurs ''threads'', qui sont exécutés selon les besoins. Si un ''thread'' est bloqué par un accès mémoire, un autre ''thread'' prend la relève. Un processeur de shader permet d'exécuter "en même temps" entre 8 et 64 ''threads''. La conséquence que l'on peut envoyer plusieurs morceaux de tableau sur un processeur de shader. Chaque morceau de tableau est combiné avec un shader pour former un ''thread'', et l'on envoie 8 à 64 de ces ''threads'' sur un même processeur de shader. Pour résumer, on découpe le travail en morceaux de taille identique, qu'on envoie à chaque processeur de shader. Un GPU moderne est une sorte de processeur multicœurs amélioré, qui gère des tableaux/vecteurs de taille variable, mais les découpe en vecteurs de taille fixe à l'exécution et répartit le tout sur des processeurs SIMD. : Il faut préciser que la terminologie du GPGPU est quelque peu trompeuse. Dans la terminologie GPGPU, un ''thread'' correspond à l'exécution d'un ''shader'' sur une seule donnée scalaire, un seul entier/flottant provenant du tableau. Beaucoup de monde s'imagine que les processeurs de ''shader'' exécutent des ''threads'', qui sont regroupés à l'exécution en ''warps'' par le matériel, mais ce n'est certainement pas ce qui se passe. ===La répartition du travail telle que définie par CUDA=== Pour les GPU NVIDIA, le processus de découpage n'est pas très bien connu. Mais on peut en avoir une idée en regardant l'interface logicielle utilisée pour le GPGPU. Chez NVIDIA, celle-ci s'appelle CUDA et ses versions donnent une idée de comment le découpage s'effectue. Premièrement, les ''shaders'' sont appelés des '''''kernels'''''. Les tableaux de taille variable sont appelés des '''''grids'''''. Les données individuelles sont appelées, de manière extrêmement trompeuse, des ''threads''. Aussi, pour éviter toute confusion, je vais renommer les ''threads CUDA'' en '''scalaires'''. Les ''grids'' sont eux-même découpés en '''''thread blocks''''', qui contiennent entre 512 et 1024 données entières ou flottantes, 512 et 1024 scalaires. La taille, 512 ou 1024, dépend de la version de CUDA utilisée, elle-même liée au modèle de carte graphique utilisé. Plutôt que d'utiliser le terme ''thread blocks'', je vais parler de '''bloc de scalaires'''. Le bloc de scalaire est une portion d'un tableau, un bloc de mémoire, une suite d'adresse consécutives. Il a donc une adresse de départ. Chaque scalaire d'un bloc de scalaire a un indice qui permet de déterminer sa position dans le tableau, qui est calculé par le processeur de commande. Le calcul de l'indice peut se faire de différentes manières, suivant que le tableau soit un tableau unidimensionnel (une suite de nombre) ou bidimensionnel (une matrice). CUDA gère les deux cas et les dernières cartes graphiques gèrent aussi des tableaux à trois dimensions. Le calcul de l'adresse d'un scalaire se fait en prenant l'adresse de départ du bloc de scalaire, et la combinant avec les indices. [[File:Block-thread.svg|centre|vignette|upright=2|Découpage des tableaux avec CUDA.]] Les processeurs de ''shader'' sont appelés des '''''streaming multiprocessor''''', terme encore une fois trompeur. Une fois lancé sur un processeur de ''shader'', le shader lit un bloc de scalaire et l'utilise pour faire ses calculs. Il y reste définitivement : il ne peut pas migrer sur un autre processeur en cours d'exécution. Un processeur de ''shader'' peut exécuter plusieurs instances de ''shaders'' travaillant sur des bloc de scalaires différents. Dans le meilleur des cas, il peut traiter en parallèle environ 8 à 16 bloc de scalaires différents en même temps. [[File:Software-Perspective for thread block.jpg|centre|vignette|upright=2|Exécution des ''thread blocks'' sur les processeurs de ''shaders''.]] Le bloc de scalaires est découpé en vecteurs d'environ 32 entiers/flottants, appelés des '''''warps''''' dans la terminologie NVIDIA/CUDA, des '''''wavefronts''''' dans la terminologie AMD. Avec 512/1024 éléments par bloc de scalaire, découpé en ''warps'' de 32, cela donne 16 à 32 ''warps'' suivant la version de CUDA utilisée. Le découpage en ''warps'' est encore une fois le fait du processeur de commande. Le terme ''warp'' est aussi utilisé pour décrire l'instance du ''shader'' qui fait des calculs avec un ''warp''. Un ''warp''/''wavefront'' est donc en réalité un ''thread'', un programme, une instance de ''shader'', qui manipule des vecteurs SIMD de 32 éléments. Les 16 à 32 ''warps'' sont exécutés en même temps sur le processeur de ''shader'', via ''multithreading'' matériel, à savoir que le processeur les exécute à tour de rôle. Un ''warp'' exécute des instructions SIMD sur des vecteurs de 32 éléments, donc de taille fixe. Pour résumer, plus un GPU contient de processeurs de ''shaders'', plus le nombre de blocs de scalaires qu'il peut traiter en même temps est important. Par contre, la taille des ''warps'' ne change pas trop et reste la même d'une génération sur l'autre. Cela ne signifie pas que la taille des vecteurs reste la même, mais elle est assez contrainte. ===La répartition avec une file de ''threads''=== Pour résumer, les tableaux sont découpés en morceaux et chaque morceau est combiné avec un shader pour former un ''thread''. reste à répartir ces ''threads'' sur les processeurs de shaders. La répartition la plus simple est la suivante : le premier morceau va dans le premier processeur de shader, le second morceau va dans le second processeur de shader, etc. En clair, un simple algorithme du tourniquet. Si elle été utilisée sur les anciens GPU, notamment sur les cartes graphiques SGI des années 80-90, ce n'est pas celle qui est utilisée aujourd'hui. A la place, la répartition demande une coopération entre le processeur de commande et les processeurs de shaders. Les GPU modernes incorporent une '''file de ''threads''''', entre le processeur de commande et les processeurs de shaders, qui sert de file d'attente. Le processeur de commande découpe les tableaux en ''threads'', qui sont ajoutés dans cette file d'attente. Les processeurs de shader récupèrent ensuite les ''threads'' disponibles dans cette file d'attente. Le processeur de commande remplit la file de ''thread'' tant que celle-ci n'est pas pleine. Et la file de ''thread'' se vide quand un processeur de shader est libre, qu'il a finit de calculer le ''thread'' précédent. La répartition est alors dynamique et exploite au mieux les processeurs de shader. Prenons l'exemple d'un GPU avec une file de ''thread'' capable de mémoriser 32 ''threads'', et 16 processeurs de shader. Soumettons un tableau de 24 éléments. Le processeur de commande remplit la file de ''thread'' avec 16 ''threads''. Les processeurs de shader lisent alors chacun un ''thread'', ce qui fait que la file d'attente se vide de 16 ''threads'', il n'en reste que 8. Une fois qu'un shader a finit son travail, il lit un ''thread'' dans la file d'attente, ce qui fait qu'elle se vide un ''thread'' après l'autre. ===L'exécution simultanée des commandes=== Maintenant, regardons ce qui se passe quand on envoie deux commandes successives. Prenons deux commandes de 24 ''threads'' chacune, avec 16 processeurs de shaders. Pour simplifier, les processeurs de shader vont d'abord exécuter les 16 ''threads'' de la première commande, et on va supposer qu'ils vont tous se terminer quasiment en même temps. Le processeur de commande remplit alors la file de commande : en plus des 8 ''threads'' restants, il rajoute 8 ''threads'' provenant de la commande suivante. Les processeurs de shader exécutent alors les 16 commandes dans la file : la moitié vient de la première commande, l'autre vient de la seconde commande. En clair, la file de ''thread'' permet une forme de "pipeline", un terme connu de ceux qui ont déjà lu un cours d'architecture des ordinateurs. L'idée est que l'on peut lancer une nouvelle commande alors que la précédente n'est pas terminée. Il s'agit de l'idée générale, mais les détails peuvent être assez surprenants. Par exemple, si on veut exécuter pleins de commandes très petites, elles peuvent s'exécuter en parallèle dans des processeurs de shader séparés. Par exemple, avec 32 processeurs de shader, vous pouvez exécuter 16 commandes de un ''thread'' chacune. Ou encore, une commande lancée récemment peut se terminer avant une commande plus ancienne. Bref : la file de ''thread'' permet de lancer plusieurs commandes l'une après l'autre, mais elles peuvent s’exécuter en même temps et se terminer dans le désordre. Tout fonctionne à la perfection tant que les commandes de calcul sont indépendantes. Mais il arrive qu'une commande prenne en entrée le résultat d'une commande précédente. Dans ce cas, lancer les deux commandes en même temps peut poser problème. La seconde commande peut alors tenter de lire un résultat qui n'a pas encore été calculé. Et il ne s'agit là que d'un cas particulier, mais de nombreuses dépendances assez complexes entre commandes existent. De telles dépendances imposent que la première soit intégralement terminée avant que la seconde démarre. On parle alors de ''partial pipeline flush''. Pour gérer cela, le processeur de commande supporte des '''commandes de synchronisation'''. Les plus simples, les commandes ''FLUSH'' empêchent le démarrage d'une commande tant que les précédentes sont en cours. Elles empêchent le processeur de commande d'ajouter des ''threads'' dans la file de ''thread'', du moins tant que celle-ci n'est pas entièrement vide, et aussi tant que les processeurs de shader sont occupés. Ces commandes de synchronisation sont aussi appelées des '''barrières GPU'''. le terme indique bien qu'elle séparent le flux de commandes en deux. Les barrières GPU ont un cout en performance, car elles empêchent d'exécuter plusieurs commandes en même temps. Mais elles sont nécessaires. Par exemple, reprenons l'exemple de deux commandes de 24 ''threads'' chacune, séparées par une barrière GPU, toujours avec 16 processeurs de shader. Le GPU va d'abord exécuter les 16 premiers ''threads''. Puis, il va exécuter les 8 ''threads'' restants de la première commande, mais pas plus. La barrière GPU l'empeche d'exécuter les ''threads'' de la commande suivante. C'est seulement ensuite qu'il lancera les 16 ''threads'' de la seconde commande, puis les 8 restants. Cela a pris plus de temps que d'exécuter les deux commandes en même temps. D'autres barrières GPU sont plus précises. Elles empêchent le démarrage d'une nouvelle commande tant que la commande précédente n'a pas atteint un certain stade de son exécution. Elles permettent de gagner en performance en démarrant la commande suivante au plus tôt. De telles commandes sont des commandes du style "attend que le registre de statut numéro N contienne la valeur adéquate avant de démarrer la commande suivante". Une alternative réserve une adresse mémoire, dans laquelle la commande précédente écrit une valeur prédéterminée pour dire qu'elle a finit. ==La répartition du travail pour le rendu graphique== Après avoir vu le cas du GPGPU, nous allons voir le cas du rendu 3D. La différence est que les commandes GPGPU sont remplacées par des commandes 3D, qui demande d'afficher un objet. Du point de vue de l'API 3D, elles correspondent grossièrement soit à un ''draw call'', soit à un changement de ''render state''. Aussi, nous ferrons parfois la confusion entre les deux, bien que ce soit techniquement une grosse simplification, plus proche de l'erreur ou de la confusion que de l'abus de langage. Les commandes graphiques sont différentes des commandes de calcul, mais elles fonctionnent sur le même principe. La différence est qu'une commande graphique demande d'afficher un objet 3D, les ''draw call'' affichant une image objet par objet. Une commande graphique contient bien un tableau de données : un tableau de triangle qui correspond à l'objet à afficher. Cependant, elle contient aussi une liste de texture et une liste de shaders. Le tableau de triangles est découpé en ''threads'' qui sont envoyés aux processeurs de shaders. Il est possible d'imaginer un GPU qui attend qu'une commande soit terminée avant de démarrer la suivante. Il s'agit d'une technique simple, mais tellement peu performante qu'il n'est pas certain qu'elle ait été utilisée en pratique. En pratique, tous les GPUs exécutent plusieurs commandes graphiques consécutives en même temps, comme pour les commandes de calcul. Par exemple, si une commande simple n'utilise que 3 processeurs de shaders sur 8, la commande suivante peut être lancée pour occuper les 5 processeurs de shader restants. Un point important est que ce n'est possible que si les circuits fixes ne sont pas un facteur limitant. Si la première commande sature les circuits fixe, le lancement de la seconde commande sera empêché. Concrètement, du point de vue de l'API graphique, le GPU peut exécuter plusieurs ''draw call'' en même temps. Pire que ça, un ''draw call'' lancé après un autre peut finir avant ! Et la conséquence, c'est que les problèmes de dépendances vus plus haut reviennent. Par exemple, imaginez qu'un jeu écrive une ''shadowmap'' dans une texture, puis l'utilise dans un algorithme d'éclairage. Une première commande calcule la ''shadowmap'', une seconde commande exécute l'algorithme d'éclairage. Il est interdit de démarrer la seconde commande tant que la première commande n'a pas calculé son résultat, car la ''shadowmap'' n'est pas encore prête. Une barrière GPU est alors nécessaire. Elle est ajoutée par le ''driver'', ou par le programmeur s'il utilise une API 3D récente (DirectX 12, Vulkan). Un autre exemple survient quand deux ''draw calls'' consécutifs utilisent des ''render state'' différents. Dans ce cas, le GPU voit deux commandes de rendu, avec une commande de changement d'état entre les deux. La commande de changement d'état fait alors deux choses : le changement d'état, mais aussi une barrière GPU. Concrètement, le processeur de commande ne démarre le second ''draw call'' que quand le changement d'état est terminé. Tout cela est géré par le processeur de commande, qui détermine dans quel ordre lancer les commandes, quand émettre des barrières, etc. On retrouve aussi des files de ''threads'', ou du moins leur équivalent pour le rendu graphique. Pour le moment, on ne voit pas de différence avec le calcul GPGPU : le processeur de commande et la file de ''thread'' sont là, elles sont exploitées de manière à exécuter des commandes graphiques en parallèle sur des processeurs de shaders, etc. Une grosse différence est qu'il y a plusieurs files de ''threads'', dont le nombre exact dépend du GPU considéré. Une autre différence est la présence de circuits fixes, à savoir un rastériseur, un ''input assembler'' et des ROPs. Voyons cela en détail. ===Les GPU avec une seule unité géométrique=== Avant de poursuivre, faisons une première remarque : un triangle est affiché sur un ou plusieurs pixels lors de l'étape de rastérisation. Un triangle peut donner quelques pixels lors de l'étape de rastérisation, alors qu'un autre va couvrir 10 fois de pixels, un autre seulement trois fois plus, un autre seulement un pixel, etc. La conséquence est qu'il y a plus de travail à faire sur les pixels que sur les sommets. C'est un phénomène d''''amplification''', qui explique qu'il y a plus de processeurs pour les ''pixel shaders'' que pour les ''vertex shaders''. Et il impact grandement la répartition du travail sur les processeurs de shaders. Le cas le plus simple est celui des GPU avec une unité géométrique, qui alimente un rastériseur, qui lui-même alimente plusieurs unités de pixel/texture. L'''input assembler'' envoie des sommets à l'unité géométrique, dès que celle-ci est inoccupée, prête à accepter une nouvelle tâche à faire. De son côté, le rastériseur distribue les pixels aux unités de pixel/texture. Les GPU de ce genre sont assez rares, et surtout assez anciens. Les premières cartes graphique de l'entreprise SGI était de ce type, la Geforce 256 de NVIDIA l'était aussi. [[File:Architecture d'un GPU tenant compte de l'amplification des pixels.png|centre|vignette|upright=2.5|Architecture d'un GPU tenant compte de l'amplification des pixels]] La seule difficulté est de gérer l'amplification des pixels, la répartition des pixels sur les unités de pixel, et c'est le rastériseur qui s'en charge. Pour cela, le rastériseur utilise une '''file de pixels''', similaire à celle utilisé pour les commandes GPGPU, à la différence qu'elle mémorise des pixels à texturer/éclairer, sans compter qu'elle est placée plus loin dans le pipeline. Le rastériseur accumule les pixels qu'il génère dans cette file de pixel, les unités de pixel piochent le travail à faire dedans. La file de pixels est techniquement une mémoire FIFO dite multiport, à savoir qu'on peut lire plusieurs pixels en une seule fois. Pour être précis, la file de pixel est gérée par un circuit de répartition (''dispatcher''), qui répartit les pixels sur les unités de pixel libres. Le circuit de répartition peut regrouper les pixels générés en paquets de 43 à 64 pixels, pour tenir compte du caractère SIMD des processeurs de shaders. Les paquets générés sont techniquement des ''threads'', similaire aux ''threads'' GPGPU, sauf qu'ils ne contiennent que des pixels, avec tout ce qu'il faut pour les texturer ou les éclairer. [[File:Dispatch des shaders sur plusieurs processeurs de shaders.png|centre|vignette|upright=2|Dispatch des shaders sur plusieurs processeurs de shaders]] Il arrive que l'unité géométrique doive attendre que le rastériseur soit disponible. Par exemple, imaginons que le rastériseur traite un gros triangle, qui occupe une centaine de pixels à l'écran. Supposons que le GPU a 4 processeurs de shaders basiques, ce qui fait qu'il traite les 100 pixels générés à la rastérisation par paquets de 4. Traiter les 100 pixels prend 25 passes en tout. Pendant ce temps, l'unité géométrique a tout le temps pour calculer plusieurs triangles. Et gérer la situation peut se faire de deux manières. La première est la moins performante : l'unité géométrique est bloquée quand le rastériseur est occupé sur de gros triangles. Elle ne peut pas recevoir de nouveaux sommets à traiter, l'''input assembler'' est lui aussi en pause. Une solution alternative met en attente les triangles générés par l'unité géométrique, dans une mémoire FIFO dédiée. Ainsi, l'unité géométrique peut accumuler des triangles dans la mémoire FIFO, préparer du travail en avance pour le rastériseur. Le rastériseur consulte la FIFO quand il est libre. La FIFO se remplit tant que l'unité de rastérisation est occupée sur des gros triangles, puis elle est vidée progressivement quand elle traite des triangles plus petits. La mémoire FIFO en question est appelée la '''file de sommets transformés'''. : Notons que si la FIFO est pleine, on n'a pas le choix : l'unité géométrique est bloquée, elle n'accepte plus de nouveau triangles. [[File:GPU basique avec FIFO avant le rastériseur.png|centre|vignette|upright=2.5|GPU basique avec FIFO avant le rastériseur]] La présence d'une FIFO permet aussi d'implémenter facilement l'assemblage de primitives. Pour rappel, les unités géométriques calculent des sommets, alors que le rastériseur prend en entrée des triangles. Les sommets doivent donc être regroupés en triangles, lors d'une étape d'assemblage des primitives, qui précède la rastérisation. Il est possible d'ajouter une seconde mémoire FIFO entre l'assemblage de primitive et les rastériseur. La FIFO en question est appelée le '''tampon de primitive''', ou encore la file de primitives. [[File:GPU basique avec FIFO avant le rastériseur - prise en compte de l'assemblage de primitives.png|centre|vignette|upright=2.5|GPU basique avec FIFO avant le rastériseur - prise en compte de l'assemblage de primitives]] Pour résumer, l’implémentation demande deux choses : * l'ajout d'une file de sommets transformés entre l'unité géométrique et le rastériseur ; * l'ajout d'une file de pixels et d'une unité de distribution en sortie du rastériseur. Ces deux modifications seront conservées dans les GPU ultérieurs, avec quelques modifications mineures. Il faut dire qu'ajouter des mémoires FIFOs a des avantages certains. La présence des mémoires FIFO désynchronise deux étapes consécutives du pipeline, tout en gardant une exécution des étapes dans l'ordre. Une étape écrit ses résultats dans la mémoire FIFO, l'étape suivante lit ce tampon quand elle démarre de nouveaux calculs, la première étape n'a pas à attendre que la seconde soit disponible pour lui envoyer des données. Sauf si la mémoire FIFO est pleine, car elle ne peut plus accepter de nouveaux sommets/pixels, évidemment. ===Les GPU avec des processeurs séparés pour les ''vertex'' et ''pixel shaders''=== Maintenant, regardons ce qui se passe si l'on ajoute plusieurs unités géométriques. Les GPU de ce type commencent avec la Geforce 2 de NVIDIA et se terminent tout de même avec la Geforce 6/7. Soit une bonne décennie de GPU de ce type. De tels GPUs sont plus simples pour plusieurs raisons, la principale étant qu'il n'ont qu'un seul circuit rastériseur. La répartition du travail avec plusieurs rastériseurs est en effet beaucoup plus compliquée qu'avec un seul. [[File:Parallélisme dans une carte 3D.png|centre|vignette|upright=3|Parallélisme dans une carte 3D]] Notons que cette séparation marche ausis pour les GPU qui ont des processeurs de shaders. De tels GPUs ont des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders''. La raison est que DirectX 9.0 et OpenGL avaient des jeux d'instruction différents pour les ''vertex'' et ''pixel shaders''. Par exemple, les ''pixels shaders'' de l'époque pouvaient accéder aux textures, pas les ''vertex shaders''. Les GPU de ce type ont beaucoup plus de processeurs de ''pixel shaders'' que de processeurs de ''vertex shaders'', en raison du phénomène d'amplification de pixels mentionné plus haut. Pour donner un exemple, la Geforce 6800 avait 16 processeurs pour les ''pixel shaders'' et 6 processeurs pour les ''vertex shaders''. Un problème de ces GPU est que la répartition entre puissance entre ''vertex'' et ''pixel shaders'' est fixe. Mais tous les jeux vidéos n'ont pas les mêmes besoins : certains sont plus lourds au niveau géométrie que d'autres, certains ont des ''pixels shaders'' très gourmands avec des ''vertex shaders'' très ''light'', d'autres font l'inverse, etc. La répartition idéale est variable d'un jeu vidéo à l'autre, voire d'un niveau de JV à l'autre, et ces GPU en étaient loin. [[File:Architecture de base d'une carte 3D - 5.png|centre|vignette|upright=1.5|Carte 3D avec pixels et vertex shaders non-unifiés.]] Ceci étant dit, voyons comment la répartition du travail se fait sur de tels GPUs. La présence de plusieurs unités géométriques a deux conséquences : il faut alimenter plusieurs unités géométriques en triangles/sommets, il faut gérer l'envoi des triangles au rastériseur. Les deux demandent des solutions distinctes. La répartition sur les processeurs de ''vertex shader'' utilise encore une fois une file dédiée, sur le même modèle que la file de ''threads'' GPGPU. La file en question est appelée la '''file de sommets non-transformés''', pour la distinguer de celle située avant l'étape de rastérisation. Les deux mémorisent des sommets, mais la première se situe avant le ''vertex shader'', l'autre après. La gestion de cette file de sommets non-transformés est le fait de l'''input assembler''. Pour remplir la file, l'''input assembler'' lit le tampon de sommets en mémoire vidéo, ainsi que le tampon d'indice. Il crée alors des paquets de sommets, qui sont envoyés aux processeurs de shaders (remplacer par les unités de T&L sur les anciens GPU). Typiquement, il utilise des paquets de 32/64 sommets. Le fait de regrouper les sommets en paquets permet de profiter de la nature SIMD des processeurs de shaders, à savoir qu'un paquet est traité en bloc par une instance de shader. Voyons maintenant ce qui se passe après les ''vertex shaders''. Les unités géométriques envoient des sommets à un rastériseur unique, qui est donc un point de convergence, où plusieurs unités géométriques envoient leurs résultats. Pour gérer cette convergence, les GPU modifient la file de sommets transformés, celle située juste avant le rastériseur. L'idée est qu'elle est connectée à tous les processeurs de ''vertex shaders'', et qu'elle peut recevoir plusieurs triangles à la fois. Rien de compliqué à cela, ce n'est pas si compliqué de créer des mémoires RAM usuelle capables de supporter plusieurs écritures simultanées. Il suffit d'ajouter des ports d'écritures à la mémoire FIFO et le tour est joué. Pour résumer, l'implémentation demande d'ajouter une file de ''thread'' en amont de l'''input assembler'', et de modifier la mémoire FIFO en amont du rastériseur. La FIFO devient une mémoire multiport et est connectée à tous les processeurs de ''vertex shader''. Encore une fois, ces deux détails vont se retrouver dans les GPU qui vont suivre, avec quelques modifications mineures. ===Les GPU avec des shaders unifiés=== Il est maintenant temps de passer aux processeurs avec des processeurs de shaders unifiés. Depuis DirectX 10, le jeu d'instruction des ''vertex shaders'' et des ''pixels shaders'' a été unifié : il n'y a plus de différences entre les deux. En conséquence, il n'y a plus de distinction entre processeurs de ''vertex shaders'' et de ''pixels shaders'', chaque processeur pouvant traiter indifféremment l'un ou l'autre. L'usage de '''''shaders'' unifiés''' permet d'adapter la répartition entre ''vertex shaders'' et ''pixels shaders'' suivant les besoins de l'application, là où la séparation entre unités de vertex et de pixel ne le permettait pas. [[File:Architecture de base d'une carte 3D - 6.png|centre|vignette|upright=1.5|Carte 3D avec ''pixels'' et ''vertex shaders'' unfifiés.]] Une implémentation simple utilise toujours un rastériseur unique et un ''input assembler''. On retrouve encore une fois une file de pixel, une file de sommets transformées et une file de sommets non-transformés. Et le tout est connecté aux processeurs de shaders. Le rastériseur est connecté en entrée comme en sortie sur tous les processeurs de shaders, l’''input assembler'' envoie des triangles à tous les processeurs de shaders, etc. Pour cela, les deux files de sommets sont connectées à tous les processeurs de ''shader'', et il en est de même pour la file de pixels. Les processeurs de shaders lisent dans ces files pour récupérer du travail à faire. Un premier problème est qu'il faut éviter qu'ils se marchent sur les pieds. Par se marcher sur les pieds, on veut dire que le rastériseur et l’''input assembler'' ne doivent pas envoyer du travail à un même processeur de shader en même temps. Si l'''input assembler'' démarre un ''vertex shader'' sur un processeur de shader, le rastériseur ne peut pas démarrer un ''pixel shader'' dessus. Une solution serait de privilégier la file de pixel sur la files de sommets non-transformés. Les processeurs de shader exécutent donc des pixels shaders en priorité sur les ''vertex shaders''. Un second problème est que l'implémentation demande beaucoup d'interconnexions. Elle marche encore quand on a peu de processeurs de shader, moins d'une trentaine. Mais au-delà, connecter un rastériseur à 30 processeurs de shader devient un véritable défi technique. Les interconnexions sont complexes à câbler, déplacer des données dedans demande beaucoup de courant, ça chauffe, la longueur des fils rend les transferts de données assez lents, la fréquence du GPU en souffre. La seule solution est d'utiliser plusieurs rastériseurs, chacun connecté à un nombre limité de processeurs de shaders. Maintenant, imaginez que le GPU incorpore plusieurs rastériseurs, afin de rastériser plus de triangles et d'améliorer les performances. Le câblage serait encore plus abominable avec des processeurs de shaders unifiés. Du moins, ce serait le cas si on souhaite connecter chaque rastériseur à tous les processeurs de shaders. Mais en réalité, il y a moyen d'utiliser plusieurs rastériseurs intelligemment. L'idée est que chaque rastériseur est connecté à un petit nombre de processeurs de shaders, pas à tous les processeurs. Par exemple, avec 35 processeurs de shaders, on peut avoir 7 rastériseurs, chacun connecté à 5 processeurs de shaders. Les interconnexions sont alors grandement simplifiées. ===Le cas des ROPs=== Nous venons de voir comment le GPU répartit le travail pour ce qui est de la rastérisation et des processeurs de shaders. Mais nous n'avons pas parlé des ROPs. Les ROPs sont techniquement des circuits fixes, qui s'occupent de la gestion du tampon de profondeur, mais aussi de certaines fonctionnalités comme l'''alpha blending''. Un GPU contient plusieurs ROPs, à l'exception de quelques GPU grand public des années 90. Et il faut connecter ces ROPs aux processeurs de shaders. Et là encore, il y a une différence entre les GPU avec des processeurs shaders unifiés et les autres. Avec des processeurs séparés pour les ''vertex shaders'' et les ''pixel shaders'', les ROPs sont connectés aux processeurs de pixels shaders. Pour cela, il y a deux possibilités. Avec la première, chaque ROP est connecté à une unité de pixel, rien d'autre. Elle a été utilisée sur de vielles cartes graphiques, dans les années 90 et avant. La seconde solution utilise des interconnexions complètes, à savoir que chaque ROP est connecté à toutes les unités de pixel. La Geforce 6800 utilisait cette solution, comme le montre le schéma précédent. Le ''fragment crossbar'' entre les processeurs de ''pixel shader'' et les ROPs est un réseau d'interconnexion, qui connecte les 16 processeurs de ''pixel shader'' aux 16 ROPs. [[File:GeForce 6800.png|centre|vignette|upright=3|GeForce 6800]] Avec les shaders unifiés, il faut connecter tous les ROPS à tous les processeurs de shaders. Le réseau d'interconnexion est juste plus complexe, car on ne connecte plus les ROPs seulement aux processeurs de pixel shader, mais à tous les processeurs de shaders. Le nombre d'interconnexion augmente donc. Et de nos jours, le nombre de ROPs et de processeurs de shaders est trop élevé pour que cette solution soit valable. La solution actuellement retenue est la même qu'avec les rastériseurs : on ne relie un ROP qu'à un nombre limité de processeurs de shaders. Et les interconnexions retenues sont les mêmes que celles utilisés pour le rastériseur. Un GPU moderne est organisé en plusieurs ''Graphic Processing Units'' (terminologie NVIDIA), que nous abrégerons en GPC. Ils regroupent chacun : plusieurs processeurs de shaders, un rastériseur et un ROP. Les processeurs de shaders envoient leurs résultats au ROP et au rasteriseur dans le GPC, pas à ceux dans un autre GPC. [[File:Graphic card architecture with unified shaders.png|centre|vignette|upright=2.5|Graphic card architecture with unified shaders]] Le fait de regrouper ainsi un rastériseur avec un ROP s'explique par le fait que les deux sont reliés entre eux. En effet, l'élimination des pixels cachés peut se faire de manière précoce, à savoir juste après la rastérisation. Les deux circuits s'enchainent alors l'un à la suite, ce qui fait qu'il vaut mieux vaut les relier ensemble. ==L'exécution de commandes différentes : graphiques et GPGPU== Les GPU modernes disposent de plusieurs processeurs de commandes, et donc de plusieurs files de commande. Et les chiffres peuvent monter assez haut. Par exemple, les GPU AMD utilisent un processeur de commande graphique, accompagné de plusieurs processeurs de commande dédiés au GPGPU. Les processeurs de commande spécifique au GPGPU étaient appelés des ACE, et il y en avait 8, 16 ou 32 selon l'architecture, le nombre ayant augmenté au cours du temps. Et chacun gérait plusieurs files de commandes séparées ! Par exemple, les GPU AMD d'architecture GCN ont 8 processeurs de commande GPGPU pour 64 files de commandes GPGPU, auxquels il faut ajouter le processeur pour les commandes graphiques en plus ! Les GPU NVIDIA d'architecture Pascal disposent eux de 32 files de commande matérielles, mais dont 31 sont réservées aux GPGPU. [[File:GCN command processing.svg|centre|vignette|upright=2|GCN command processing]] Mais quelle est l'intérêt ? Pour le dire vite : remplir des processeurs de shader inutilisés. Il arrive que des processeurs de shaders soient inutilisés, soit parce que la commande en cours d'exécution n'en a pas besoin. Il est théoriquement possible de les remplir avec des ''threads'' provenant de la commande suivante. Cependant, ce n'est pas toujours possible. Par exemple, il se peut que la commande suivante soit une barrière GPU, qui bloque l'avancée des commandes suivantes. Ou encore, que la file de commande soit vide, car les commandes suivantes ne sont pas encore arrivées. Une solution serait alors de chercher des commandes ailleurs, et de préférence des commandes capables de remplir des processeurs de shader. Mais où les trouver ? ===L'exécution simultanée de plusieurs applications sur le GPU=== La solution la plus simple est assez évidente, quand on se rappelle du chapitre sur les API 3D, et notamment de la section de fin sur le partage du GPU entre plusieurs applications. Un GPU est aujourd'hui sollicité par plusieurs logiciels en même temps : il est possible de lancer un jeu vidéo en fenêtré, pendant que OBS ou un logiciel de ''Streaming'' capture l'écran, avec Firefox et Discord et un programme de ''cloud computing'' de type ''Folding@Home'' qui tournent en arrière-plan. Et toutes ces applications sont accélérées par le GPU. Des situations de ce genre, où on doit partager le GPU entre plusieurs applications, sont assez courantes. Et c'est soit le pilote qui s'en charge, soit le GPU lui-même. * L''''arbitrage logiciel''' est la méthode la plus simple : tout est fait en logiciel. Concrètement, le système d'exploitation et/ou le pilote de la carte graphique se chargent du partage du GPU. Dans les deux cas, tout est fait en logiciel. * L''''arbitrage matériel''' délègue ce partage au GPU. Du moins en partie, car le système d'exploitation détermine quelle application a la priorité. L'avantage est que cela décharge le processeur d'une tâche assez lourde en calcul, pour la déporter sur le GPU. Sans compter que les mécanismes matériels d'arbitrage sont plus efficaces. Sur Windows, avant l'arrivée du modèle de driver dit ''Windows Display Driver Model'', il n'y avait pas d'arbitrage entre les applications. Il y avait une file de commande unique et les commandes étaient exécutées en mode "premier entré, premier sorti". Et ce n'était pas un problème car les seules applications qui utilisaient le GPU étaient des jeux vidéos fonctionnant en mode plein écran. Avec le modèle de driver ''Windows Display Driver Model'' (WDDM), l'arbitrage logiciel est apparu. Depuis 2020, avec l'arrivée de la ''Windows 10 May 2020 update'', Windows supporte l'arbitrage matériel. L'arbitrage matériel est implémenté grâce à la présence de plusieurs processeurs de commandes. L'idée est d'attribuer des priorités à chaque processeurs de commande. Et plus un processeur de commande est prioritaire, plus le GPU lui réserve de processeurs de shaders. Prenons l'exemple avec deux processeurs de commande, et donc deux files de ''thread'', et 16 processeurs de shaders. Si la première file de ''thread'' a la priorité, le GPU lui réserve 14 processeurs de shaders sur 16, contre seulement 2 pour l'autre file. Un point important est qu'en général, une seule application effectue un rendu 3D, typiquement un jeu vidéo en plein écran ou en fenêtré. Les autres applications utilisent plutôt le GPGPU ou des commandes de rendu 2D, qui utilisent uniquement les processeurs de shaders. En conséquence, pas besoin d'avoir plusieurs processeurs de commande généralistes. L'idéal est de n'avoir qu'un seul processeur de commandes graphiques, accompagné de pleins de processeurs de commandes dédiés au GPGPU. Cela permet d'exécuter des commandes graphiques et GPGPU en parallèle. Les priorités étaient autrefois statiques, à savoir que certaines processeurs de commande avaient toujours la priorité sur tous les autres. En théorie, le processeur de commande graphique devrait avoir la priorité sur les autres. Mais il y a des exceptions. Par exemple, sur les cartes graphiques avant l'architecture AMD RDNA 1, c'était l'inverse : les tâches de GPGPU avaient la priorité sur le rendu graphique. Maintenant, les GPU récents ont un système de priorité plus complexe, dynamique, qui choisit les priorité en fonction des besoins. ===L'usage de plusieurs processeurs de commande pour le rendu 3D=== Utiliser plusieurs applications est une première solution pour utiliser plusieurs files de commande. Mais avec un peu d'huile de coude, il est possible d'extraire plusieurs files de commande par application. le rendu 3D permet ce genre de choses, car de nombreux ''draw calls'' sont indépendants. Pour comprendre en quoi traiter plusieurs commandes peut être utile, je vais reprendre l'exemple décris sur cet [https://therealmjp.github.io/posts/breaking-down-barriers-part-3-multiple-command-processors/ article de blog], les liens sur l'ensemble des posts sur le sujet sont à la fin de ce chapitre. En rendu 3D, il est fréquent que des ''draw call'' consécutifs soient dépendants, qu'ils doivent s'exécuter l'un après l'autre, sans recouvrement possible. Un exemple est celui des filtres de post-traitement comme le Bloom ou la profondeur de champ. Ces deux filtres se font en plusieurs étapes, qui se traduisent en plusieurs ''draw call'' consécutifs. Les ''draw calls'' d'un filtre sont dépendants, à savoir que chaque étape lit le résultat de l'étape précédente. Ils sont donc séparés par des barrières GPU, ce qui ruine rapidement les performances. De plus, leurs ''draw calls'' n'utilisent pas tous les processeurs de shaders. Un filtre de bloom basique laisse des processeurs de shaders libre, qui pourraient être utilisés pour exécuter un autre filtre de post-traitement en parallèle, comme un filtre de profondeur de champ. Le problème est que la file de commande est séquentielle par nature. Une solution serait pour le moteur graphique de mélanger les ''draw call'' pour le filtre de bloom et ceux du filtre de profondeur de champ, mais ce serait assez compliqué pour les programmeurs. Une autre solution délègue le problème au matériel et à l'API. L'idée est d'avoir deux files de ''thread'' : une pour le filtre de Bloom, une autre pour le filtre de profondeur de champ. Les processeurs de shaders peuvent prendre des commandes dans les deux files. S'il y a assez de processeurs de shaders de libre, ils peuvent piocher des commandes dans la seconde file de commande. Les deux files accumulent des commandes, séparées par des barrières GPU. Mais les barrières GPU se limitent à l'intérieur d'une file de commande, elles n'ont pas d'impact sur l'autre file de commande. Mais qui dit deux files de commande dit : deux processeurs de commande et files de commandes ! Les deux processeurs de commande alimentent les deux files de ''thread'', en piochant chacun dans une file de commande dédiée. Et la même logique vaut pour N processeurs de commandes : tant qu'il y a autant de files de ''thread'' et de files de commande, la logique fonctionne à l'identique. La seule contrainte est d'alimenter plusieurs files de commande, ce qui est le job du pilote du GPU. De plus, il faut ajouter des ''commandes de synchronisation entre files de commandes'', qui permettent de synchroniser les deux files de commande. La plus simple force à attendre que les deux files de commandes soient vidées avant de démarrer une nouvelle commande. ===Le support dans les API graphiques modernes=== Avant l'apparition des API modernes Vulkan et DirectX 12, l'usage de plusieurs files de commande était peu fréquent. En effet, les applications ne voyaient pas de file de commande, les API graphiques avaient juste des ''draw calls'' et les commandes graphiques étaient envoyées au pilote une par une. C'est ce dernier qui remplissait la file de commande avec des commandes matérielles. En pratique, le pilote pouvait utiliser une file de commande par application, guère plus. Il n'y avait aucun moyen d'utiliser plusieurs files de commande par application. Pire que ça : un moteur graphique ne pouvait pas utiliser plusieurs cœurs. Les API graphiques de l'époque étaient séquentielles par nature, elles exécutaient les ''draw calls'' l'un après l'autre, et il ne pouvait y avoir qu'une seule instance par application. DirectX 11 a bien tenté d'ajouter des mécanismes pour multi-threader les moteurs graphiques, mais ils étaient difficiles à utiliser. Les jeux vidéo de l'époque étaient capables d'utiliser plusieurs cœurs, mais le moteur graphique n'en utilisait qu'un seul. Typiquement, le rendu du son était réalisé sur un cœur CPU, le reste sur un autre. Parfois, on séparait le moteur physique et le moteur graphique sur deux cœurs séparés, mais pas plus. Pour aider les programmeurs, les API modernes Vulkan et DirectX 12, gèrent nativement plusieurs files de commandes, qui sont exposées au programmeur. Ce qui était autrefois le domaine du pilote du GPU a été déporté dans les API graphiques. Les commandes remplacent les ''draw calls'' mais fonctionnent plus ou moins de la même manière. Le programmeur a accès à une fonction pour créer une file de commande, une autre pour ajouter une commande dedans, et une pour envoyer la file de commande au pilote de GPU. Le programmeur peut remplir ces files de commande comme il le souhaite, tant que les commandes dans des files séparées sont indépendantes. Il faut noter que ce sont des files de ''commandes graphiques'', pas des files de ''commande matérielles''. En clair, les files de commandes sont envoyées au pilote du GPU, qui les traduit en commandes matérielles. Et le GPU gère ses propres files de commandes matérielles. Il peut envoyer ses files de commandes séparément dans des processeurs de commande séparés, mais il peut décider d'accumuler toutes les commandes dans une seule file de commande matérielle si le GPU n'a qu'un seul processeur de commande, ou n'utiliser qu'une seule file de commande par application. En théorie, ce système permet de réduire l'usage du processeur. Au lieu d'appeler le pilote de GPU à chaque ''draw call'', il peut accumuler plein de ''draw call'' dans une file de commande et envoyer le tout en une seule fois au pilote. Il est donc possible de lancer un grand nombre de ''draw calls'' sans trop surcharger le CPU. Du moins en théorie, car le problème des ''draw call'' très petits que le GPU exécute trop vite reste présent. Mieux que ça, ce système permet de couper le moteur graphique en plusieurs ''threads'', afin de l'exécuter sur plusieurs cœurs. Par exemple, deux ''threads'' peuvent créer deux files de commandes différentes qui sont exécutées en parallèle (si elles ne sont pas sérialisées par le pilote de GPU). Tout complexifie la tâche du programmeur, vu qu'il doit faire le travail autrefois pris en charge par le pilote de GPU. L'intérêt est que cela permet au programmeur d'optimiser. Il peut utiliser plusieurs processeurs de commande par application, peut exécuter des commandes en parallèle pour remplir tous les processeurs de shaders, etc. Et surtout, il peut insérer des barrières GPU seulement quand elles sont nécessaires. Avec DirectX 11, le pilote de GPU avait tendance à être prudent. Il insérait des barrières GPU très souvent, et n'avait pas moyen de savoir lesquelles étaient réellement nécessaires et celles qui étaient juste inutiles. Il n'avait pas accès aux algorithmes des programmeurs, pour optimiser le tout. Les programmeurs ont la possibilité d'être plus efficaces, du moins s'ils sont assez compétents et qu'on leur laisse le temps. Il faut impérativement que les files puissent exécuter des commandes séparément sans que cela pose problème. Il y a bien des barrières GPU inter-files, mais laissons cela de côté. Toujours est-il que les files de commandes en question accumulent des commandes graphiques, pas des commandes matérielles. Mais le pilote de la carte graphique reçoit ces files de commande graphique et les accumule dans ses propres files de commandes matérielles. Avec DirectX 12, trois types différents de files de commande sont nativement supportés : GRAPHIC, COMPUTE et COPY. COPY correspond aux transferts DMA ou à des copies de données en VRAM, COMPUTE mémorise des commandes GPGPU, GRAPHIC est une file de commande de rendu 2D/3D. Du moins, c'est l'explication simplifiée. Car en réalité, les commandes GPGPU sont capables de faire tout ce que COPY permet, et les commandes GRAPHIC supportent tout ce que les commandes COMPUTE supportent. Les relations entre les trois sont inclusives : COPY est inclus dans COMPUTE, qui est lui-même inclus dans GRAPHIC. Les applications envoient des files de commande COPY, COMPUTE et GRAPHIC au pilote de GPU, qui décide quoi en faire. Si le GPU le supporte, il envoie les commandes GRAPHIC au processeur de commandé généraliste, les commandes COMPUTE à ceux dédiés au GPGPU, les commandes COPY au contrôleur DMA. Mais il peut aussi envoyer transformer des commandes COMPUTE en commandes GRAPHICS ou des commandes CPY en COMPUTE ou GRAPHIC, si le besoin s'en fait sentir. Si le GPU n'a qu'un seul processeur de commande, les commandes sont simplement remises en série et envoyées au seul processeur de commande. C'est le cas sur les GPU Intel intégrés : ils ont un simple processeur de commande qui fait tout. Ils ne gèrent pas les transferts DMA, car ils sont reliés à la RAM système (mémoire unifiée). Vulkan utilise un système différent. Vulkan permet de demander à la carte graphique combien elle a de processeurs de commande et quelles commandes ils supportent. Si un GPU n'a qu'un seul processeur de commande, Vulkan n'en verra qu'un et le code devra être adapté pour. En clair, la gestion des processeurs de commande est totalement délégué au programmeur. Il n'y a pas d'abstraction comme avec DirectX 12, mais une gestion fine du matériel. ==Sources extérieures== Pour compléter la lecture de ce chapitre, vous pouvez lire les 6 articles de blog suivants : * [https://therealmjp.github.io/posts/breaking-down-barriers-part-1-whats-a-barrier/ What's a barrier]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-2-synchronizing-gpu-threads/ Synchronizing GPU Threads]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-3-multiple-command-processors/ Multiple Command Processorsr]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-4-gpu-preemption/ GPU Preemption]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-5-back-to-the-real-world/ Back To The Real World]. * [https://therealmjp.github.io/posts/breaking-down-barriers-part-6-experimenting-with-overlap-and-preemption/ Experimenting With Overlap and Preemption]. {{NavChapitre | book=Les cartes graphiques | prev=Le processeur de commandes | prevText=Le processeur de commandes | next=Le pipeline géométrique : évolution | nextText=Le pipeline géométrique : évolution }} {{autocat}} e9xhcgkwdcpytp1mhc4hi4rqijo7sks Les cartes graphiques/Le support matériel du lancer de rayons 0 80578 763816 749765 2026-04-16T20:25:57Z Mewtow 31375 /* Un historique rapide des cartes graphiques dédiées au lancer de rayon */ 763816 wikitext text/x-wiki Les cartes graphiques actuelles utilisent la technique de la rastérisation, qui a été décrite en détail dans le chapitre sur les cartes accélératrices 3D. Mais nous avions dit qu'il existe une seconde technique générale pour le rendu 3D, totalement opposée à la rastérisation, appelée le '''lancer de rayons'''. Cette technique a cependant été peu utilisée dans les jeux vidéo, jusqu'à récemment. La raison est que le lancer de rayons demande beaucoup de puissance de calcul, sans compter que créer des cartes accélératrices pour le lancer de rayons n'est pas simple. Mais les choses commencent à changer. Quelques jeux vidéos récents intègrent des techniques de lancer rayons, pour compléter un rendu effectué principalement en rastérisation. De plus, les cartes graphiques modernes incorporent quelques circuits pour accélérer le lancer de rayons, même s'ils restent marginaux des compléments au rendu par rastérisation. S'il a existé des cartes accélératrices totalement dédiées au rendu en lancer de rayons, elles sont restées confidentielles. Aussi, nous allons nous concentrer sur les cartes graphiques récentes, et allons peu parler des cartes accélératrices dédiées au lancer de rayons. ==Le lancer de rayons== Le lancer de rayons et la rastérisation. commencent par générer la géométrie de la scène 3D, en plaçant les objets dans la scène 3D, et en effectuant l'étape de transformation des modèles 3D, et les étapes de transformation suivante. Mais les ressemblances s'arrêtent là. Le lancer de rayons effectue l'étape d'éclairage différemment, sans compter qu'il n'a pas besoin de rastérisation. ===Le ''ray-casting'' : des rayons tirés depuis la caméra=== La forme la plus simple de lancer de rayon s'appelle le '''''ray-casting'''''. Elle émet des lignes droites, des '''rayons''' qui partent de la caméra et qui passent chacun par un pixel de l'écran. Les rayons font alors intersecter les différents objets présents dans la scène 3D, en un point d'intersection. Le moteur du jeu détermine alors quel est le point d'intersection le plus proche, ou plus précisément, le sommet le plus proche de ce point d'intersection. Ce sommet est associé à une coordonnée de textures, ce qui permet d'associer directement un texel au pixel associé au rayon. [[File:Raytrace trace diagram.png|centre|vignette|upright=2|Raycasting, rayon simple.]] En somme, l'étape de lancer de rayon et le calcul des intersections remplacent l'étape de rasterisation, mais les étapes de traitement de la géométrie et des textures existent encore. Après tout, il faut bien placer les objets dans la scène 3D/2D, faire diverses transformations, les éclairer. On peut gérer la transparence des textures assez simplement, si on connait la transparence sur chaque point d'intersection d'un rayon, information présente dans les textures. [[File:Anarch short gameplay.gif|vignette|Exemple de rendu en ray-casting 2D dans un jeu vidéo.]] En soi, cet algorithme est simple, mais il a déjà été utilisé dans pas mal de jeux vidéos. Mais sous une forme simple, en deux dimensions ! Les premiers jeux IdSoftware, dont Wolfenstein 3D et Catacomb, utilisaient cette méthode de rendu, mais dans un univers en deux dimensions. Cet article explique bien cela : [[Les moteurs de rendu des FPS en 2.5 D\Le moteur de Wolfenstein 3D|Le moteur de rendu de Wolfenstein 3D]]. Au passage, si vous faites des recherches sur le ''raycasting'', vous verrez que le terme est souvent utilisé pour désigner la méthode de rendu de ces vieux FPS, alors que ce n'en est qu'un cas particulier. [[File:Simple raycasting with fisheye correction.gif|centre|vignette|upright=2|Simple raycasting with fisheye correction]] ===Le ''raytracing'' proprement dit=== Le lancer de rayon proprement dit est une forme améliorée de ''raycasting'' dans la gestion de l'éclairage et des ombres est modifiée. Le lancer de rayon calcule les ombres assez simplement, sans recourir à des algorithmes compliqués. L'idée est qu'un point d'intersection est dans l'ombre si un objet se trouve entre lui et une source de lumière. Pour déterminer cela, il suffit de tirer un trait entre les deux et de vérifier s'il y a un obstacle/objet sur le trajet. Si c'est le cas, le point d'intersection n'est pas éclairé par la source de lumière et est donc dans l'ombre. Si ce n'est pas le cas, il est éclairé avec un algorithme d'éclairage. Le trait tiré entre la source de lumière et le point d'intersection est en soi facile : c'est rayon, identique aux rayons envoyés depuis la caméra. La différence est que ces rayons servent à calculer les ombres, ils sont utilisés pour une raison différente. Il faut donc faire la différence entre les '''rayons primaires''' qui partent de la caméra et passent par un pixel de l'écran, et les '''rayon d'ombrage''' qui servent pour le calcul des ombres. [[File:Ray trace diagram.svg|centre|vignette|upright=2|Principe du lancer de rayons.]] Les calculs d'éclairage utilisés pour éclairer/ombrer les points d'intersection vous sont déjà connus : la luminosité est calculée à partir de l'algorithme d'éclairage de Phong, vu dans le chapitre "L'éclairage d'une scène 3D : shaders et T&L". Pour cela, il faut juste récupérer la normale du sommet associé au point d'intersection et l'intensité de la source de lumière, et de calculer les informations manquantes (l'angle normale-rayons de lumière, autres). Il détermine alors la couleur de chaque point d'intersection à partir de tout un tas d'informations. ===Le ''raytracing'' récursif=== [[File:Glasses 800 edit.png|vignette|Image rendue avec le lancer de rayons récursif.]] La technique de lancer de rayons précédente ne gère pas les réflexions, les reflets, des miroirs, les effets de spécularité, et quelques autres effets graphiques de ce style. Pourtant, ils peuvent être implémentés facilement en modifiant le ''raycasting'' d'une manière très simple. Il suffit de relancer des rayons à partir du point d'intersection. La direction de ces '''rayons secondaires''' est calculée en utilisant les lois de la réfraction/réflexion vues en physique. De plus, les rayons secondaires peuvent eux-aussi créer des rayons secondaires quand ils sont reflétés/réfractés, etc. La technique est alors appelée du ''lancer de rayons récursif'', qui est souvent simplement appelée "lancer de rayons". [[File:Recursive raytracing.svg|centre|vignette|upright=2|Lancer de rayon récursif.]] ==Les optimisations du lancer de rayons== Les calculs d'intersections sont très gourmands en puissance de calcul. Sans optimisation, on doit tester l'intersection de chaque rayon avec chaque triangle. Mais diverses optimisations permettent d'économiser des calculs. Elles consistent à regrouper plusieurs triangles ensemble pour rejeter des paquets de triangles en une fois. Pour cela, la carte graphique utilise des '''structures d'accélération''', qui mémorisent les regroupements de triangles, et parfois les triangles eux-mêmes. ===Les volumes englobants=== [[File:BoundingBox.jpg|vignette|Objet englobant : la statue est englobée dans un pavé.]] L'idée est d'englober chaque objet par un pavé appelé un ''volume englobant''. Le tout est illustré ci-contre, avec une statue représentée en 3D. La statue est un objet très complexe, contenant plusieurs centaines ou milliers de triangles, ce qui fait que tester l'intersection d'un rayon avec chaque triangle serait très long. Par contre, on peut tester si le rayon intersecte le volume englobant facilement : il suffit de tester les 6 faces du pavé, soit 12 triangles, pas plus. S'il n'y a pas d'intersection, alors on économise plusieurs centaines ou milliers de tests d'intersection. Par contre, s'il y a intersection, on doit vérifier chaque triangle. Vu que les rayons intersectent souvent peu d'objets, le gain est énorme ! L'usage seul de volumes englobant est une optimisation très performante. Au lieu de tester l'intersection avec chaque triangle, on teste l'intersection avec chaque objet, puis l'intersection avec chaque triangle quand on intersecte chaque volume englobant. On divise le nombre de tests par un facteur quasi-constant, mais de très grande valeur. Il faut noter que les calculs d'intersection sont légèrement différents entre un triangle et un volume englobant. Il faut dire que les volumes englobant sont généralement des pavées, ils utilisent des rectangles, etc. Les différences sont cependant minimales. Mais on peut faire encore mieux. L'idée est de regrouper plusieurs volumes englobants en un seul. Si une dizaine d'objets sont proches, leurs volumes englobants seront proches. Il est alors utile d'englober leurs volumes englobants dans un super-volume englobant. L'idée est que l'on teste d'abord le super-volume englobant, au lieu de tester la dizaine de volumes englobants de base. S'il n'y a pas d'intersection, alors on a économisé une dizaine de tests. Mais si intersection, il y a, alors on doit vérifier chaque sous-volume englobant de base, jusqu'à tomber sur une intersection. Vu que les intersections sont rares, on y gagne plus qu'on y perd. Et on peut faire la même chose avec les super-volumes englobants, en les englobant dans des volumes englobants encore plus grands, et ainsi de suite, récursivement. On obtient alors une '''hiérarchie de volumes englobants''', qui part d'un volume englobant qui contient toute la géométrie, hors skybox, qui lui-même regroupe plusieurs volumes englobants, qui eux-mêmes... [[File:Example of bounding volume hierarchy.svg|centre|vignette|upright=2|Hiérarchie de volumes englobants.]] Le nombre de tests d'intersection est alors grandement réduit. On passe d'un nombre de tests proportionnel aux nombres d'objets à un nombre proportionnel à son logarithme. Plus la scène contient d'objets, plus l'économie est importante. La seule difficulté est de générer la hiérarchie de volumes englobants à partir d'une scène 3D. Divers algorithmes assez rapides existent pour cela, ils créent des volumes englobants différents. Les volumes englobants les plus utilisés dans les cartes 3D sont les '''''axis-aligned bounding boxes (AABB)'''''. La hiérarchie est mémorisée en mémoire RAM, dans une structure de données que les programmeurs connaissent sous le nom d'arbre, et précisément un arbre binaire ou du moins d'un arbre similaire (''k-tree''). Traverser cet arbre pour passer d'un objet englobant à un autre plus petit est très simple, mais a un défaut : on saute d'on objet à un autre en mémoire, les deux sont souvent éloignés en mémoire. La traversée de la mémoire est erratique, sautant d'un point à un autre. On n'est donc dans un cas où les caches fonctionnent mal, ou les techniques de préchargement échouent, où la mémoire devient un point bloquant car trop lente. ===La cohérence des rayons=== Les rayons primaires, émis depuis la caméra, sont proches les uns des autres, et vont tous dans le même sens. Mais ce n'est pas le cas pour des rayons secondaires. Les objets d'une scène 3D ont rarement des surfaces planes, mais sont composés d'un tas de triangles qui font un angle entre eux. La conséquence est que deux rayons secondaires émis depuis deux triangles voisins peuvent aller dans des directions très différentes. Ils vont donc intersecter des objets très différents, atterrir sur des sources de lumières différentes, etc. De tels rayons sont dits '''incohérents''', en opposition aux rayons cohérents qui sont des rayons proches qui vont dans la même direction. Les rayons incohérents sont peu fréquents avec le ''rayctracing'' récursif basique, mais deviennent plus courants avec des techniques avancées comme le ''path tracing'' ou les techniques d'illumination globale. En soi, la présende de rayons incohérents n'est pas un problème et est parfaitement normale. Le problème est que le traitement des rayons incohérent est plus lent que pour les rayons cohérents. Le traitement de deux rayons secondaires voisins demande d'accéder à des données différentes, ce qui donne des accès mémoire très différents et très éloignés. On ne peut pas profiter des mémoires caches ou des optimisations de la hiérarchie mémoire, si on les traite consécutivement. Un autre défaut est qu'une même instance de ''pixels shaders'' va traiter plusieurs rayons en même temps, grâce au SIMD. Mais les branchements n'auront pas les mêmes résultats d'un rayon à l'autre, et cette divergence entrainera des opérations de masquage et de branchement très couteuses. Pour éviter cela, les GPU modernes permettent de trier les rayons en fonction de leur direction et de leur origine. Ils traitent les rayons non dans l'ordre usuel, mais essayent de regrouper des rayons proches qui vont dans la même direction, et de les traiter ensemble, en parallèle, ou l'un après l'autre. Cela ne change rien au rendu final, qui traite les rayons en parallèle. Ainsi, les données chargées dans le cache par le premier rayon seront celles utilisées pour les rayons suivants, ce qui donne un gain de performance appréciable. De plus, cela évite d'utiliser des branchements dans les ''pixels shaders''. Les méthodes de '''tri de rayons''' sont nombreuses, aussi en faire une liste exhaustive serait assez long, sans compter qu'on ne sait pas quelles sont celles implémentées en hardware, ni commetn elles le sont. ===Les avantages et désavantages comparé à la rastérisation=== [[File:ViewFrustum.svg|vignette|upright=1|Volume délimité par la caméra (''view frustum'').]] L'avantage principal du lancer de rayons est la détermination des surfaces visibles. La rastérisation a tendance à calculer inutilement des portions non-rendues de la scène 3D, malgré l'usage de techniques de ''culling'' ou de ''clipping'' aussi puissantes qu'imparfaites. De nombreux fragments/pixels sont éliminés à la toute fin du pipeline, grâce au z-buffer, après avoir été calculés et texturés. Le lancer de rayons ne calcule pas les portions invisibles de l'image par construction : pas besoin de ''culling'', de ''clipping'', ni même de z-buffer. D'ailleurs, l'absence de z-buffer réduit grandement le nombre d'accès mémoire. Les réflexions et la réfraction sont gérées naturellement par le lancer de rayon récursif, de même que l'éclairage et les ombres, alors qu'elles demandent des ruses de sioux pour obtenir un résultat correct avec la rastérisation. Avec la rastérisation, cela demande d'utiliser des techniques de rendu au-dessus de la rastérisation de base, comme des ombres volumétriques, des ''lightmaps'', des ''shadowmaps'' ou autres. L'absence de ''shadowmaps'', qui demande de faire du ''render-to-texture'', élimine certaines écritures mémoires et permet aux caches de texture d'être en lecture seule (leurs circuits sont donc assez simples et performants, les problèmes de cohérence des caches disparaissent). Mais tous ces avantages sont compensés par le fait que le lancer de rayons est plus lent sur un paquet d'autres points. Déjà, les calculs d'intersection sont très lourds, ils demandent beaucoup de calculs et d'opérations arithmétiques, plus que pour l'équivalent en rastérisation. Et ils se parallélisent mal. Le moteur de jeu doit d'abord lancer les rayons primaires, puis les rayons secondaires qui en découlent, puis les rayons de la troisième passe, etc. Au final, cela se parallélise assez mal, car il y a un ordre de traitement des rayons (primaires, puis secondaires, puis...), alors que la rastérisation traite chaque triangle/pixel/source de lumière de manière indépendante. Ensuite, le lancer de rayon n'est pas économe niveau accès mémoire. Ce qu'on économise avec l’absence de tampon de profondeur et l'absence de ''shadowmaps'', on le perd au niveau de la BVH et des accès aux textures. La BVH est notamment un énorme problème. Les BVH étant ce que les programmeurs connaissent sous le nom d'arbre (binaire ou k-tree), elles dispersent les données en mémoire, alors que les GPU et CPU modernes préfèrent des données consécutives en RAM. Leur hiérarchie mémoire est beaucoup plus efficace pour accéder à des données proches en mémoire qu'à des données dispersées et il n'y a pas grand chose à faire pour changer la donne. La traversée d'une BVH se fait avec des accès en mémoire vidéo complétement désorganisés, là où le rendu 3D a des accès plus linéaires qui permettent d'utiliser des mémoires caches. ==Le matériel pour accélérer le lancer de rayons== En théorie, il est possible d'utiliser des ''shaders'' pour effectuer du lancer de rayons, mais la technique n'est pas très performante. Il vaut mieux utiliser du hardware dédié. Heureusement, cela ne demande pas beaucoup de changements à un GPU usuel. Le lancer de rayons se différencie peu de la rastérisationla différence principale étant que l'étape de rastérisation est remplacée par une étape de lancer de rayons. Les deux types de rendu ont besoin de calculer la géométrie, d'appliquer des textures et de faire des calculs d'éclairage. Sachant cela, les GPU actuels ont juste besoin d'ajouter des circuits dédiés au lancer de rayons, à savoir des unités de génération des rayons et des unités pour les calculs d'intersection. ===Les circuits spécialisés pour les calculs liés aux rayons=== Toute la subtilité du lancer de rayons est de générer les rayons et de déterminer quels triangles ils intersectent. La seule difficulté est de gérer la transparence, mais aussi et surtout la traversée de la BVH. En soit, générer les rayons et déterminer leurs intersections n'est pas compliqué. Il existe des algorithmes basés sur du calcul vectoriel pour déterminer si intersection il y a et quelles sont ses coordonnées. C'est toute la partie de parcours de la BVH qui est plus compliquée à implémenter : faire du ''pointer chasing'' en hardware n'est pas facile. Et cela se ressent quand on étudie comment les GPU récents gèrent le lancer de rayons. Ils utilisent des shaders dédiés, qui communiquent avec une '''unité de lancer de rayon''' dédiée à la traversée de la BVH. Le terme anglais est ''Ray-tracing Unit'', ce qui fait que nous utiliseront l'abréviation RTU pour la désigner. Les shaders spécialisés sont des '''shaders de lancer de rayon''' et il y en a deux types. * Les '''''shaders'' de génération de rayon''' s'occupent de générer les rayons * Les '''''shaders'' de ''Hit/miss''''' s'occupent de faire tous les calculs une fois qu'une intersection est détectée. Le processus de rendu en lancer de rayon sur ces GPU est le suivant. Les shaders de génération de rayon génèrent les rayons lancés, qu'ils envoient à la RTU. La RTU parcours la BVH et effectue les calculs d'intersection. Si la RTU détecte une intersection, elle lance l'exécution d'un ''shader'' de ''Hit/miss'' sur les processeurs de ''shaders''. Ce dernier finit le travail, mais il peut aussi commander la génération de rayons secondaires ou d'ombrage. Suivant la nature de la surface (opaque, transparente, réfléchissante, mate, autre), ils décident s'il faut ou non émettre un rayon secondaire, et commandent les shaders de génération de rayon si besoin. [[File:Implementation hardware du raytracing.png|centre|vignette|upright=2|Implémentation hardware du raytracing]] ===La RTU : la traversée des structures d'accélérations et des BVH=== Lorsqu'un processeur de shader fait appel à la RTU, il lui envoie un rayon encodé d'une manière ou d'une autre, potentiellement différente d'un GPU à l'autre. Toujours est-il que les informations sur ce rayon sont mémorisées dans des registres à l'intérieur de la RTU. Ce n'est que quand le rayon quitte la RTU que ces registres sont réinitialisés pour laisser la place à un autre rayon. Il y a donc un ou plusieurs '''registres de rayon''' intégrés à la RTU. La RTU contient beaucoup d''''unités de calculs d'intersections''', des circuits de calcul qui font les calculs d'intersection. Il est possible de tester un grand nombre d'intersections de triangles en parallèles, chacun dans une unité de calcul séparée. L'algorithme de lancer de rayons se parallélise donc, au moins un minimum, et la RTU en profite. En soi, les circuits de détection des intersections sont très simples et se résument à un paquet de circuits de calcul (addition, multiplications, division, autres), connectés les uns aux autres. Il y a assez peu à dire dessus. Mais les autres circuits sont très intéressants à étudier. Il y a deux types d'intersections à calculer : les intersections avec les volumes englobants, les intersections avec les triangles. Volumes englobants et triangles ne sont pas encodés de la même manière en mémoire vidéo, ce qui fait que les calculs à faire ne sont pas exactement les mêmes. Et c'est normal : il y a une différence entre un pavé pour le volume englobant et trois sommets/vecteurs pour un triangle. La RTU des GPU Intel, et vraisemblablement celle des autres GPU, utilise des circuits de calcul différents pour les deux. Elle incorpore plus de circuits pour les intersections avec les volumes englobants, que de circuits pour les intersections avec un triangle. Il faut dire que lors de la traversée d'une BVH, il y a une intersection avec un triangle par rayon, mais plusieurs pour les volumes englobants. L'intérieur de la RTU contient aussi de quoi séquencer les accès mémoire nécessaires pour parcourir la BVH. Traverser un BVH demande de tester l'intersection avec un volume englobant, puis de décider s'il faut passer au suivant, et rebelotte. Une fois que la RTU tombe sur un triangle, elle l'envoie aux unités de calcul d'intersection dédiées aux triangles. Les RTU intègrent des '''caches de BVH''', qui mémorisent des portions de la BVH au cas où celles-ci seraient retraversées plusieurs fois de suite par des rayons consécutifs. La taille de ce cache est de l'ordre du kilo-octet ou plus. Pour donner des exemples, les GPU Intel d'architecture Battlemage ont un cache de BVH de 16 Kilo-octets, soit le double comparé aux GPU antérieurs. Le cache de BVH est très fortement lié à la RTU et n'est pas accesible par l'unité de texture, les processeurs de ''shader'' ou autres. [[File:Raytracing Unit.png|centre|vignette|upright=2|Raytracing Unit]] Pour diminuer l’impact sur les performances, les cartes graphiques modernes incorporent des circuits de tri pour regrouper les rayons cohérents, ceux qui vont dans la même direction et proviennent de triangles proches. Ce afin d'implémenter les optimisations vues plus haut. ===La génération des structures d'accélération et BVH=== La plupart des cartes graphiques ne peuvent pas générer les BVH d'elles-mêmes. Pareil pour les autres structures d'accélération. Elles sont calculées par le processeur, stockées en mémoire RAM, puis copiées dans la mémoire vidéo avant le démarrage du rendu 3D. Cependant, quelques rares cartes spécialement dédiées au lancer de rayons incorporent des circuits pour générer les BVH/AS. Elles lisent le tampon de sommet envoyé par le processeur, puis génèrent les BVH complémentaires. L'unité de génération des structures d'accélération est complétement séparée des autres unités. Un exemple d'architecture de ce type est l'architecture Raycore, dont nous parlerons dans les sections suivantes. Cette unité peut aussi modifier les BVH à la volée, si jamais la scène 3D change. Par exemple, si un objet change de place, comme un NPC qui se déplace, il faut reconstruire le BVH. Les cartes graphiques récentes évitent de reconstruire le BVH de zéro, mais se contentent de modifier ce qui a changé dans le BVH. Les performances en sont nettement meilleures. ==Un historique rapide des cartes graphiques dédiées au lancer de rayon== Les premières cartes accélératrices de lancer de rayons sont assez anciennes et datent des années 80-90. Leur évolution a plus ou moins suivi la même évolution que celle des cartes graphiques usuelles, sauf que très peu de cartes pour le lancer de rayons ont été produites. Par même évolution, on veut dire que les cartes graphiques pour le lancer de rayon ont commencé par tester deux solutions évidentes et extrêmes : les cartes basées sur des circuits programmables d'un côté, non programmables de l'autre. La première carte pour le lancer de rayons était la TigerShark, et elle était tout simplement composée de plusieurs processeurs et de la mémoire sur une carte PCI. Elle ne faisait qu'accélérer le calcul des intersections, rien de plus. Les autres cartes effectuaient du rendu 3D par voxel, un type de rendu 3D assez spécial que nous n'avons pas abordé jusqu'à présent, dont l'algorithme de lancer de rayons n'était qu'une petite partie du rendu. Elles étaient destinées au marché scientifique, industriel et médical. On peut notamment citer la VolumePro et la VIZARD et II. Les deux étaient des cartes pour bus PCI qui ne faisaient que du ''raycasting'' et n'utilisaient pas de rayons d'ombrage. Le ''raycasting'' était exécuté sur un processeur dédié sur la VIZARD II, sur un circuit fixe implémenté par un FPGA sur la Volume Pro Voici différents papiers académiques qui décrivent l'architecture de ces cartes accélératrices : * [https://citeseerx.ist.psu.edu/document?repid=rep1&type=pdf&doi=ccda3712ba0e6575cd08025f3bb920de46201cac The VolumePro Real-Time Ray-Casting System]. * [https://www.researchgate.net/publication/234829060_VIZARD_II_A_reconfigurable_interactive_volume_rendering_system VIZARD II: A reconfigurable interactive volume rendering system] La puce SaarCOR (Saarbrücken's Coherence Optimized Ray Tracer) et bien plus tard par la carte Raycore, étaient deux cartes réelement dédiées au raycasting pur, sans usage de voxels, et étaient basées sur des FPGA. Elles contenaient uniquement des circuits pour accélérer le lancer de rayon proprement dit, à savoir la génération des rayons, les calculs d'intersection, pas plus. Tout le reste, à savoir le rendu de la géométrie, le placage de textures, les ''shaders'' et autres, étaient effectués par le processeur. Voici des papiers académiques sur leur architecture : * [https://gamma.cs.unc.edu/SATO/Raycore/raycore.pdf RayCore: A ray-tracing hardware architecture for mobile devices]. Le successeur de la SaarCOR, le Ray Processing Unit (RPU), était une carte hybride : c'était une SaarCOR basée sur des circuits fixes, qui gagna quelques possibilités de programmation. Elle ajoutait des processeurs de ''shaders'' aux circuits fixes spécialisés pour le lancer de rayons. Les autres cartes du genre faisaient pareil, et on peut citer les cartes ART, les cartes CausticOne, Caustic Professional's R2500 et R2100. Les cartes 3D modernes permettent de faire à la fois de la rasterisation et du lancer de rayons. Pour cela, les GPU récents incorporent des unités pour effectuer des calculs d'intersection, qui sont réalisés dans des circuits spécialisés. Elles sont appelées des RT Cores sur les cartes NVIDIA, mais les cartes AMD et Intel ont leur équivalent, idem chez leurs concurrents de chez Imagination technologies, ainsi que sur certains GPU destinés au marché mobile. Peu de choses sont certaines sur ces unités, mais il semblerait qu'il s'agisse d'unités de textures modifiées. De ce qu'on en sait, certains GPU utilisent des unités pour calculer les intersections avec les triangles, et d'autres unités séparées pour calculer les intersections avec les volumes englobants. La raison est que, comme dit plus haut, les algorithmes de calcul d'intersection sont différents dans les deux cas. Les algorithmes utilisés ne sont pas connus pour toutes les cartes graphiques. On sait que les GPU Wizard de Imagination technology utilisaient des tests AABB de Plücker. Ils utilisaient 15 circuits de multiplication, 9 additionneurs-soustracteurs, quelques circuits pour tester la présence dans un intervalle, des circuits de normalisation et arrondi. L'algorithme pour leurs GPU d'architecture Photon est disponible ici, pour les curieux : [https://www.highperformancegraphics.org/slides23/2023-06-_HPG_IMG_RayTracing_2.pdf]. {{NavChapitre | book=Les cartes graphiques | prev=Les écritures en VRAM hors ROPs | prevText=Les écritures en VRAM hors ROPs | next=L'antialiasing | prevNext=L'antialiasing }}{{autocat}} k70a8qsq70f5xbykqwz07dqjt00ry3e Mathc initiation/a458 0 80896 763864 763520 2026-04-17T11:50:23Z Xhungab 23827 763864 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] : [[Mathc initiation/005s| Sommaire]] : {{Partie{{{type|}}}| L'intégrale de flux de surface définie paramétriquement simplifiée}} : En mathématiques, une intégrale de surface est une intégrale définie sur toute une surface qui peut être courbe dans l'espace. Pour une surface donnée, on peut intégrer sur un champ scalaire ou sur un champ vectorie [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-parametrization/v/introduction-to-parametrizing-a-surface-with-two-parameters Khanacademy : introduction to parametrizing a surface with two parameters] ... ... ... [https://fr.khanacademy.org/math/multivariable-calculus/integrating-multivariable-functions/surface-integrals-introduction/v/introduction-to-the-surface-integral Khanacademy : introduction to the surface integral] : L''''intégrale de surface''' définie paramétriquement : (b (v(s) int( int( ||R_s x R_t|| dtds = (a (u(s) L''''intégrale de flux de surface''' définie paramétriquement : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| '''||R_s x R_t||''' dtds = (a (u(s) '''||R_s x R_t||''' L''''intégrale de flux de surface''' définie paramétriquement '''simplifiées''' : (b (v(s) int( int( f(Rx(s,t), Ry(s,t), Rz(s,t)) . |R_s x R_t| dtds = (a (u(s) <br> Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/a451|x_afile.h ............ Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/a452|x_rsxrt.h ............ Calcul de R_s_x_R_t au point p]] * [[Mathc initiation/a453|x_pfs_ts.h ......... L'intégrale de flux de surface]] '''définie paramétriquement simplifiées''' : * [[Mathc initiation/a454|x_pfs_st.h ......... L'intégrale de flux de surface]] : <br> les fonctions f : * [[Mathc initiation/a455|f.h]] : <br> Exemples d'application : * [[Mathc initiation/a456|c00a.c .... dtds ]] * [[Mathc initiation/a457|c00b.c .... dsdt ]] {{AutoCat}} r2vywe9qqrcc629y112z0wovhjh67hh Mathc initiation/a452 0 80898 763857 716359 2026-04-17T11:11:40Z Xhungab 23827 763857 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a458| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_rsxrt.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_rsxrt.h */ /* ---------------------------------- */ /* with R = Rx i + Ry j + Rz k | i j k | | Rx_s Ry_s Rz_s | | Rx_t Ry_t Rz_t | R_s x R_t = (Ry_s * Rz_t - Ry_t * Rz_s) i - (Rx_s * Rz_t - Rx_t * Rz_s) j + (Rx_s * Ry_t - Rx_t * Ry_s) k */ /* ---------------------------------- */ v3d R_s_x_R_t( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), pt2d p ) { v3d V; V.i = fxy_x((*P_Ry),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Ry),H,p)*fxy_x((*P_Rz),H,p); V.j = (-(fxy_x((*P_Rx),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Rz),H,p)) ); V.k = fxy_x((*P_Rx),H,p)*fxy_y((*P_Ry),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Ry),H,p); return(V); } /* ---------------------------------- */ /* ---------------------------------- */ double NR_s_x_R_t( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), pt2d p ) { v3d V; V.i = fxy_x((*P_Ry),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Ry),H,p)*fxy_x((*P_Rz),H,p); V.j = (-(fxy_x((*P_Rx),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Rz),H,p)) ); V.k = fxy_x((*P_Rx),H,p)*fxy_y((*P_Ry),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Ry),H,p); return(sqrt(V.i*V.i + V.j*V.j + V.k*V.k)); } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} howqekyrwr55nzhuop0s9akv3k332kl 763858 763857 2026-04-17T11:12:00Z Xhungab 23827 763858 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_rsxrt.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_rsxrt.h */ /* ---------------------------------- */ /* with R = Rx i + Ry j + Rz k | i j k | | Rx_s Ry_s Rz_s | | Rx_t Ry_t Rz_t | R_s x R_t = (Ry_s * Rz_t - Ry_t * Rz_s) i - (Rx_s * Rz_t - Rx_t * Rz_s) j + (Rx_s * Ry_t - Rx_t * Ry_s) k */ /* ---------------------------------- */ v3d R_s_x_R_t( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), pt2d p ) { v3d V; V.i = fxy_x((*P_Ry),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Ry),H,p)*fxy_x((*P_Rz),H,p); V.j = (-(fxy_x((*P_Rx),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Rz),H,p)) ); V.k = fxy_x((*P_Rx),H,p)*fxy_y((*P_Ry),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Ry),H,p); return(V); } /* ---------------------------------- */ /* ---------------------------------- */ double NR_s_x_R_t( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), pt2d p ) { v3d V; V.i = fxy_x((*P_Ry),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Ry),H,p)*fxy_x((*P_Rz),H,p); V.j = (-(fxy_x((*P_Rx),H,p)*fxy_y((*P_Rz),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Rz),H,p)) ); V.k = fxy_x((*P_Rx),H,p)*fxy_y((*P_Ry),H,p) - fxy_y((*P_Rx),H,p)*fxy_x((*P_Ry),H,p); return(sqrt(V.i*V.i + V.j*V.j + V.k*V.k)); } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} ag6pgx65j3qx0mjnc6lehnkh84alrfl Mathc initiation/a453 0 80899 763859 716360 2026-04-17T11:13:40Z Xhungab 23827 763859 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a458| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_pfs_ts.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_pfs_ts.h */ /* ---------------------------------- */ double paraFSint_ts( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), double (*P_fi)(double x, double y, double z), double (*P_fj)(double x, double y, double z), double (*P_fk)(double x, double y, double z), double (*P_u)(double x), double (*P_v)(double x), int ny, double x ) { v3d V; pt2d p; int i = 0; double m = 0.; double M = 0.; for(i = 0; i <= ny; i++) { if(i ==0 || i== ny){m = 1.;} else if(fmod(i,2) == 0){m = 2.;} else {m = 4.;} p.y = ((*P_u)(x)) + i*(((*P_v)(x))-((*P_u)(x)))/ny; p.x = x; V = R_s_x_R_t((*P_Rx),(*P_Ry), (*P_Rz),p); M += m * ( (*P_fi)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.i + (*P_fj)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.j + (*P_fk)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.k ); } return( ((((*P_v)(x)) -((*P_u)(x)))*M) / (3*ny) ); } /* ---------------------------------- */ double parametricFS_ts( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), double (*P_fi)(double x, double y, double z), double (*P_fj)(double x, double y, double z), double (*P_fk)(double x, double y, double z), double (*P_u)(double x), double (*P_v)(double x), int ny, double ax, double bx, int nx ) { int i = 0; double m = 0.; double M = 0.; for(i = 0; i <= nx; i++) { if(i ==0 || i== nx){m = 1.;} else if(fmod(i,2) == 0){m = 2.;} else {m = 4.;} M += m * paraFSint_ts( (*P_Rx),(*P_Ry),(*P_Rz), (*P_fi),(*P_fj),(*P_fk), (*P_u),(*P_v),ny, (ax + i*(bx-ax)/nx)); } return( ((bx -ax)*M) / (3*nx) ); } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} 3wusc02zskyqy9x0rlf6i2gxxmfh50c Mathc initiation/a454 0 80900 763860 716361 2026-04-17T11:14:40Z Xhungab 23827 763860 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/a458| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_pfs_st.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_pfs_st.h */ /* ---------------------------------- */ double paraFSint_st( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), double (*P_fi)(double x, double y, double z), double (*P_fj)(double x, double y, double z), double (*P_fk)(double x, double y, double z), double (*P_u)(double y), double (*P_v)(double y), int nx, double y ) { v3d V; pt2d p; int i = 0; double m = 0.; double M = 0.; for(i = 0; i <= nx; i++) { if(i ==0 || i== nx){m = 1.;} else if(fmod(i,2) == 0 ){m = 2.;} else {m = 4.;} p.x = ((*P_u)(y)) + i*(((*P_v)(y))-((*P_u)(y)))/nx; p.y = y; V = R_s_x_R_t((*P_Rx),(*P_Ry), (*P_Rz),p); M += m * ( (*P_fi)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.i + (*P_fj)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.j + (*P_fk)((*P_Rx)(p.x,p.y), (*P_Ry)(p.x,p.y), (*P_Rz)(p.x,p.y)) * V.k ); } return( ((((*P_v)(y)) -((*P_u)(y)))*M) / (3*nx) ); } /* ---------------------------------- */ double parametricFS_st( double (*P_Rx)(double s, double t), double (*P_Ry)(double s, double t), double (*P_Rz)(double s, double t), double (*P_fi)(double x, double y, double z), double (*P_fj)(double x, double y, double z), double (*P_fk)(double x, double y, double z), double (*P_u)(double y), double (*P_v)(double y), int nx, double ay, double by, int ny ) { int i = 0; double m = 0.; double M = 0.; for(i = 0; i <= ny; i++) { if(i ==0 || i== ny){m = 1.;} else if(fmod(i,2) == 0){m = 2.;} else {m = 4.;} M += m * paraFSint_st( (*P_Rx),(*P_Ry),(*P_Rz), (*P_fi),(*P_fj),(*P_fk), (*P_u),(*P_v), nx, (ay + i*(by-ay)/ny)); } return( ((by -ay)*M) / (3*ny) ); } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} 5z9wbs321rxi6vz3qtqxbso6vzvqt2m Utilisateur:Matthius/Conseils Chrétiens 2 81988 763853 743784 2026-04-17T06:30:09Z Ziv 119944 ([[c:GR|GR]]) [[File:BrugghenDoubtingThomas.jpeg]] → [[File:De ongelovige Thomas Rijksmuseum SK-A-3908.jpeg]] → File replacement: Updating from an old version to a newer version with better quality ([[c:c:GR]]) 763853 wikitext text/x-wiki {{Brouillon}} {{Suppression}} = Conseils pour les Chrétiens = <center> Matthieu Giroux Coaching Scientifique par des Solutions Éditions LIBERLOG Éditeur n° 978-2-9531251 ISBN E-Book 9791092732795 ISBN Livre 9791092732788 [http://www.archive.org/details/ConseilsChretiens www.archive.org/details/ConseilsChretiens] Droits d'auteur 2021 Licence Creative Common by SA </center> == À Lire == Je vous conseille les liseuses à encre électronique tactiles non affiliées à un site web de vente de livrels. Même si elles sont plus chères, elles vous permettront de lire l'ensemble des auteurs présents dans mes livres. Les liseuses tactiles ou scribeuses permettent d’écrire pour créer ce qu’on veut pour plus tard. Quiconque peut devenir scribeur donc peut agir mondialement. Un écrivain public utilise les scribeuses pour écrire mondialement. Les images viennent de wikimedia commons. Bonnes lectures et écritures ! = Préface = Ce livre est une réponse aux conférences du pasteur Miki HARDI, ainsi qu’aux émissions radios de RCF Alpha et Radio Notre Dame, aux émissions des vlogs Dieu Sauve et Frère Paul Adrien. = Des Sacrements = Si on a franchi une étape on peut la sanctifier avec un sacrement. Les 7 sacrements de l’Église ne sont pas les sacrements que je présente ici. J’utilise le mot sacrement ici pour une étape à confirmer par l’Église. Le sacrement des malades n’est pas mentionné ici. C’est un des sept sacrements de l’Église. Seulement un patient peut aussi se tourner vers les autres sacrements. == Le Baptême == Lundi 27 septembre 2021 [[Image:Pierre Puget - Le baptême de Clovis.jpg|alt=|droite|vignette|upright|<center>Baptême de Clovis</center>]] Jésus s’est fait baptiser pour aller chercher la vérité. Le baptême consiste donc à aller chercher la vérité, que ce soit par les parents ou par soi-même. On cherche sa vérité et la vérité avec les autres, pour développer les autres et se développer soi. On fournit les connaissances qu’il s’agit de lire aux autres. Il y a par exemple Maître Eckhart qui donne un chemin ou Henry Charles Carey qui contredit la décroissance. On crée alors des cartes de visite pour faire plus simple. === Mes Notes === Écrire des poèmes sur sa vérité. == La Confirmation == Lundi 27 septembre 2021 Devenir prophète est l’aboutissement d’un chrétien. Ainsi la confirmation demande au chrétien d’essayer de devenir prophète, c’est à dire d’envisager un avenir sain. Un prophète parle ou écrit mondialement sur la société ou les autres. Il combat le libéralisme qui vise à individualiser l’individu quand on est en récession. Il vous permet d’être aidé par votre ou la vérité. Le prophète se place toujours derrière Dieu. [[Image:Elisabeth Keyser - A Confirmand in Normandy - NM 7478 - Nationalmuseum.jpg|alt=|droite|vignette|upright|<center>Une confirmande – Élisabeth Keyser</center>]] Pour discerner un prophète il faut être habitué à voir le vrai et le faux dans une parole de vérité. Le prophète dit vrai sur tout car il est aidé par l’esprit. Le prophète est quelqu’un qui a porté sa croix en disant la vérité. Le prophète aime. Une prophétie met du temps à être réalisée. Le prophète ne connaît pas exactement le plan de Dieu. Il le devine parce qu’il est dans la vérité, vérité qui peut surprendre en bien. Ainsi, le seul prophète qui restera à l’apocalypse selon Saint Jean sera Jésus. Un prophète intervient quand il y a du chaos. Si le chaos est mondial, alors le prophète est aidé par Jésus. Le prophète est celui qui construit le monde idéal avec les autres en les convaincant. Il a les solutions qui permettent de construire ce monde. Un prophète fait donc de la politique. On écrit des poèmes puis on écrit sur soi pour relire ces écrits plus tard. On discute avec les autres pour trouver la vérité, puis on donne la vérité qu’on pense juste. On essaye d’anticiper l’avenir en trouvant des relations avec la Bible ou avec des livres de développement personnel par la société. Puis on apprend la communication, communication qui sera une de nos intelligences. On milite pour un monde à construire. On écoute ceux qui sont en accord avec la Bible. Alors notre vie avec les autres et notre chemin de vérité fera de nous peut-être un prophète. Pour changer, il faut être ouvert à la correction. Il faut donc avoir envie d’évoluer. Le prophète n’est pas un beau parleur mais quelqu’un qui a écouté. === Mes Notes === Essayez d’envisager l’avenir ou écrivez un poème. == Le Mariage == Lundi 27 septembre 2021 [[Image:Mariage de Louis de France, duc de Bourgogne.jpg|alt=|droite|vignette|upright|<center>Mariage de Louis de France, duc de Bourgogne</center>]] Deux époux mariés ne doivent faire qu’un. L’amour ne suffit donc pas pour se marier. Serons-nous toujours chacun de notre côté après le mariage ? Ça n’est pas le mariage qui unit. Les deux fiancés doivent être unis avant de se marier. Les émotions doivent rester de côté pour prendre des décisions. Ainsi on écrit des poèmes pour comprendre ses émotions. Il faut savoir dire ce qui ne nous convient pas ou donner la bonne voie à suivre. C’est ce que font les amis. Il s’agit donc de réfléchir tout haut avec son conjoint. On doit savoir résister au monde du diable ensemble. Si sa famille va dans le mauvais chemin, il s’agit de résister. Il ne faut savoir protéger sa vie des mauvais chemins en évitant les personnes malsaines. Si on les aime, il s’agit de les écouter mais pas de les suivre. Si son conjoint à subi un échec, pas trop grave, c’est normalement plus facile de lui parler car il est plus à l’écoute. Sinon, il s’agit d’envisager que sans bon chemin, il y aura un échec. Alors, il s’agit de deviner cela et de devenir prophète sur son conjoint. Ainsi des conflits seront évités. === Mes Notes === Écrire sur ceux que vous aimez. == Devenir Prêtre == Lundi 27 septembre 2021 [[Image:Prêtre célébrant une messe.jpg|alt=|droite|vignette|upright|<center>Prêtre célébrant une messe.</center>]] Beaucoup de chrétiens et d’athées voient le prêtre comme quelqu’un à part, quelqu’un d’éthique et de moral, plus que soi-même. Le prêtre est très souvent un juste qui a décidé de se donner à Jésus. Il exprime donc le message de Dieu mais reste aussi juste que vous et moi. L’église décidera si vous serez prêtre. Suivre la théologie permettra de parcourir le chemin de vérité pour devenir prêtre. === Mes Notes === Créer un atelier pour la bonne nouvelle qu’on veut donner. == Devenir Moine ou Religieuse == Lundi 27 septembre 2021 [[Image:Eugénie Guillou.jpg|alt=|droite|vignette|upright|<center>Religieuse</center>]] Il ne s’agit pas de se forcer à devenir moine ou religieuse. Le moine ou la religieuse a une vie calme mais remplie, une vie en dehors des activités de la ville. Mais il ou elle médite sur tout ce qui l’environne en priant. Alors le calme l’atteint. Il ou elle voit Dieu en s’oubliant. Certaines fois des personnes se réfugient dans les monastères parce qu’ils en ont marre du monde libéral où le plus fort a raison. Ces personnes ne pourraient normalement pas devenir moine ou religieuses. Mais elles sont acceptées parce qu’on peut convertir petit à petit. Les moines et religieuses n’aiment pas le monde libéral. Ils ou elles savent ce qu’est vivre en société. Il ou elles retrouvent une société fraternelle en devenant moine ou religieuse. === Mes Notes === Chercher à trouver le calme. = La Religion = Il s'agit d'apporter la paix et le développement. Voyons les contradictions et allons chercher vérité sur des religions mises à mal. == Le Péché Originel == Lundi 1er Novembre 2021 [[Image:Hubble ultra deep field high rez edit1.jpg|alt=|droite|vignette|upright|<center>Notre univers est organisé</center>]] Le péché originel c’est en réalité le fait que l’humain est imparfait. Dieu a été engendré par l’univers ou un autre Dieu. Il a ensuite généré des anges, dont le diable, diable qui représente le péché originel. Le diable qui séduit l’humain, c’est sur Terre, pas dans le monde de Dieu. Les humains ont chassé le diable du monde de Dieu, comme le dit Jésus dans les évangiles. Dieu a en réalité créé notre univers quand il a vu que le diable gênait les autres anges, avec la quête de pouvoir du diable. Dieu s’est en réalité rendu compte que les anges pouvaient être imparfaits. Donc il a attendu que notre univers crée des êtres intelligents pour s’occuper du diable, sachant qu’il fait défiler le temps comme il veut. En effet, c’est une vitesse infinie qui a créé la première vie. RegarderPourquoi un Dieu ? À ce propos. === Mes Notes === Écrire à propos du premier univers. == À propos des Juifs et Jésus == jeudi 23 septembre 2021 [[Image:Jesus with the cross in Duomo (San Gimignano).jpg|alt=|droite|vignette|upright|<center>Des juifs ont aidé Christ et sont devenus chrétiens.</center>]] Longtemps les chrétiens ont réprimé les juifs parce qu’ils auraient tué un Dieu. Tout d’abord, Jésus était juif. Donc Jésus a été dénoncé par les siens. Ensuite, les chrétiens ont convaincu les juifs. Il est donc possible que le juif qui a dénoncé Jésus à Pilate, pour savoir si Jésus résistait à la mort pour sa crucifixion, ait ensuite été convaincu de devenir chrétien. Aussi, Dieu a voulu que Jésus soit dénoncé par les siens pour ne pas mettre les peuples les uns contre les autres. La rançon sert à dire que l’on perdra tout ce que l’on a possédé sur Terre signifie que les financiers n’iront que rarement dans le monde de Dieu. Le monde de Dieu se méfie de ceux qui ne pensent qu’à eux-mêmes, comme par exemple à dire qu’on serait le peuple élu, que ce soit pour les chrétiens ou pour les juifs. Dieu veut accueillir les justes. Il préfère un chrétien dans l’âme plutôt qu’un chrétien dans les normes. === Mes Notes === Discuter avec les autres religions. == À propos de l’Islam == Samedi 25 septembre 2021 [[Image:Islamic marriage.jpg|alt=|droite|vignette|upright|<center>Mariage islamique</center>]] Outre les fanatiques de la religion qui veulent entrer en contradictions avec vous, on rencontre plus souvent des personnes douces qu’on oublie. Les fidèles de l’Islam qui vivent dans notre pays veulent s’intégrer, tout comme les chrétiens non prioritaires dans d’autres pays le font. La religion sert à apporter et porter la paix. Donc ne nous écharpons pas entre religions. Au contraire, découvrir une nouvelle religion enrichit historiquement donc théologiquement. Que c’est beau de discuter entre religions. === Mes Notes === Discuter avec les autres religions. = La Plénitude = == Se Mettre en Mouvement == Lundi 6 décembre 2021 La vie c’est le mouvement. Par exemple, si nous ne faisons rien, nous oublions. La vie ne peut rester telle qu’elle était il y a peu. Donc être dans le mouvement de la vie est primordial. C’est la créativité qui permet d’être dans le mouvement de la vie. Oser imaginer son avenir permet d’anticiper l’avenir. Écrire permet de se mettre dans le mouvement de la vie. Méditer permet d’anticiper de futures situations, pour peu qu’on ne regarde pas des images rapides. Faire bouger son corps permet de sentir la vie. On cherche pour trouver des solutions et poser les vrais problèmes. Si on n’a pas de solutions, on se dit que Dieu en a. Donc c’est à nous de trouver les solutions. Par exemple, le CO2 utilise 0,04 % de l’atmosphère. Les plantes grandissent plus vite depuis qu’il augmente. Donc il n’y a pas assez de CO2, contrairement à ce qui est dit. En effet, il y avait 1,7 % de CO2 pendant le Crétacé, au moment où les plantes poussaient vite. Suivre le mouvement de la vie s’est s’accaparer sa vie, sentir qu’on vit, savoir réagir correctement à une mauvaise situation, en étant préparé, en se demandant pourquoi on vit, pourquoi on est là à cet instant. Un ami nous aide à voir cela. Vouloir devenir parfait permet de chercher la perfection de Dieu. Nous ne serons jamais parfait. C’est pourquoi poser des questions pour sentir la perfection permet de se sublimer. Vouloir devenir parfait permet de comprendre le mouvement de la vie. On devient un génie en comprenant le mouvement de la vie et de la perfection. Se parfaire c’est savoir agir avec les solutions au moment où le problème vient. Si on a su réagir correctement à une mauvaise situation, alors ça veut dire qu’on comprend la nature, qu’on est scientifique donc. Chacun a sa façon de procéder. Lire sur ses intelligences multiples permet de voir comment on pense. === Mes Notes === Écrire sur les autres et leur nature bénéfique. == La Distraction == Jeudi 21 octobre 2021 On aime se distraire. Seulement beaucoup de distractions sont malfaisantes en temps de récession. L’objectif de la finance est de faire de nous des animaux par les émotions pour que nous nous tapions dessus entre nous. Si l’état dirige réellement avec sa monnaie publique, alors on trouve des émissions créatives. Seulement, les émissions animales auront fait de nous un animal. Les documentaires peuvent avoir des informations vraies en temps de récession. Les informations sont vraies quand votre pays se développe. Il y a des émissions de divertissement qui mettent en valeur la créativité humaine. Seulement on passe beaucoup de temps à attendre ces émissions en temps de récession. === Mes Notes === Écrire sur ce que vous regardez. == La Peur == Jeudi 21 octobre 2021 [[Image:The Man Made Mad with Fear by Gustave Courbet.jpg|alt=|droite|vignette|upright|<center>Gustave Courbet - L'homme ayant peur</center>]] La peur empêche de voir le plan de Dieu pour nous. Il faut donc la comprendre. On a tous peur de quelque chose. Mais c’est anormal d’avoir peur de la première chose venue. La distraction par les émotions amène la peur de tout. Avoir peur de Dieu est une bonne peur. La crainte de Dieu est normale. Il nous a créé. Concrétiser sa peur et l’écrire permet de la voir plus tard comme quelque chose de commun. Alors on identifie le problème qui crée la peur. Pour résoudre une peur qui revient trop souvent, il s’agit de se mettre en état de réflexion afin que l’esprit nous tourne pas en rond sur la peur. === Mes Notes === Écrire sur sa peur. == Méditer == Samedi 25 septembre 2021 La méditation est importante. On relie son esprit pour qu’il soit un. La méditation permet d’être serein pour pouvoir improviser quand un païen s’interroge. Alors notre dialogue est bref et ciblé. C’est normal. Il s’agit de ne pas parler dans le vide. Vous serez étonné de savoir comment vous avez convaincu. Les gens s’intéressent aux autres. C’est votre histoire qui intéresse. Alors prier en méditant permet de se rapprocher des autres. === Mes Notes === Après avoir fait une activité, méditer pour recréer les liens dans l’esprit. Lire Devenir un Génie. == Prier == Lundi 27 septembre 2021 [[Image:Bernadette Soubirous en 1861 photo Bernadou 4.jpg|alt=|droite|vignette|upright|<center>Bernadette SOUBIROUS en train de prier.</center>]] Prier c’est penser aux autres. Le faire un tout petit peu est déjà grand. On se force au début puis on pense un tout petit peu aux autres. Prier pour agir c’est encore mieux. Alors on pense aux autres dans les moments de repos, quand on médite. Alors on agit avec les autres. Puis un jour on naît de nouveau. On trouve des solutions plus facilement alors. Après on prie sans s’en rendre compte. === Mes Notes === Prier. == Avec les Émotions == dimanche 26 septembre 2021 [[Image:Aimée Thibault - The King of Rome (Napoleon II), as a child, writing to his father (Napoleon I).jpg|alt=|droite|vignette|upright|<center>Aimée Thibault - Napoléon II en train d'écrire</center>]] Quand on est sous le choc on est prêt à tout enregistrer pour y repenser plein de fois. Ainsi dire « Dieu t’aime » à quiconque n’importe quand permet à celui qui écoute de se sentir aimé. Il est probable alors qu’il s’en souviendra pour la vie. On vit dans un monde d’émotions. Les émotions peuvent apporter la guerre. Seulement, on vit longtemps donc on ne veut pas partir en guerre. Alors l’amour prend toute sa place, surtout si on n’est pas habitué à recevoir de l’amour. En effet, les émotions ne sont rien face à l’amour du prochain. Évidemment, on commence par dire qu’on aime ses proches. Sinon il y aura des jaloux. N’oubliez pas qu’un couple ne doit faire qu’un pour ne pas choisir avec les émotions. Si vous voyez le moindre frein à ne pas choisir votre futur conjoint, réfléchissez pour savoir si vous ne ferez qu’un avec lui. Écrire des poèmes pour comprendre ses émotions permet de se voir comme un autre plus tard. Alors on peut se dire si on était fou ou simplement dans la bonne voie en pensant à ce qui nous mène. === Mes Notes === Écrire un poème. == La Chair ou l’Esprit == Jeudi 21 octobre 2021 Tant qu’on est dans la chair, on a peur d’être libre. Au début de sa liberté, on a très peur. Mais on trouve ensuite des nouveaux repères. On est alors dans l’esprit. On peut voir la liberté comme quelque chose de grand, que ce soit avant ou après que son cœur soit dans l’esprit. La liberté est pourtant apeurante. Il ne s’agit pas de s’obliger à être libre. On ne le serait pas. Il s’agit d’aimer et d’aller vers l’amour, celui qui libère. On va contre la tentation de se laisser faire. [[Image:Drago - Piero di Cosimo - Andromeda Perseo.jpg|alt=|droite|vignette|upright|<center>Piero di Cosimo – Les divertissements nous font croire n’importe quoi.</center>]] Beaucoup s’arrêtent à la peur d’être libre, c’est à dire de se voir retirer les chaînes du monde du diable. Alors on se complaît dans la chair, se disant que ceux qui sont libres n’y ont pas droit, ce qui est faux. On a surtout comme Dieu la chair ou l’argent. Au contraire, quand on est libre, le plaisir de la chair est amplifié. Seulement on se rend compte qu’il est répétitif. Alors le plaisir de la chair vient quand on veut simuler quelque chose de nouveau. Nous ne sommes pas tentés par quelque chose de plus fort que nous. La loi permet de suivre le chemin de vérité. Les dix commandements sont surtout faits pour ceux qui cherchent à devenir libres. Ensuite il y a « aime ton prochain comme toi même. ». Quand on est dans l’esprit, nos sens se développent par la méditation, le dialogue ou l’écriture. C’est pour cela que la chair est encore plus présente. Seulement, notre esprit agit plus donc on préfère être dans l’esprit. Nos sens sont mieux perçus quand nous sommes libres et par notre âge. === Mes Notes === Écrire sur la liberté. == J’ai une Addiction == Jeudi 21 octobre 2021 [[Image:MacauOpiumSmoking20c.jpg|alt=|droite|vignette|upright|<center>L'Opium est addictif.</center>]] Ceux qui n’ont jamais eu d’addiction grave sont capables de vivre sans avoir peur d’être addict. Ils vivent sereinement. Par contre, quand on a une addiction grave, la chair et la vie sont vus différemment. Ce manque ne sera mineur que quand nous deviendrons justes, à notre mort. C’est une fois libre que son cœur dit qu’on ne veut plus de l’addiction. Là le cœur a été formé. Les autres addicts sont reconnaissants de vos paroles. Seulement il faut avant que le cœur parle former le cœur et s’empêcher d’aller vers l’addiction. Encore une fois, on se crée une loi et on la respecte. Si cette loi est brisée on la promulgue de nouveau. === Mes Notes === Écrire sur ce que vous aimez le plus faire. = L’Ego = Notre ego est imparfait dès la naissance. Au début, notre ego veut se satisfaire de lui-même. Seulement, en découvrant les autres on se découvre soi. Donc on remarque que notre ego empêche de se voir. Si on veut défier sa peur, on va de plus en plus vers les autres pour évoluer vers un être social. == Je me sens Égoïste == vendredi 12 novembre 2021 [[Image:Wilhelm Marstrand, Fra et romersk osteria, piberygende jægere og italierinder, udateret, 0216NMK, Nivaagaards Malerisamling.jpg|alt=|droite|vignette|upright|<center>On peut prendre des décisions en petit comité. Wilhelm Marstrand.</center>]] L’égoïsme permet de déceler l’intérêt général. En effet l’individualisme ne permet pas de créer de nouveaux droits alors que l’intérêt général le permet. Donc quelqu’un qui défend l’intérêt général veut de nouveaux droits pour lui-même. C’est une vision à long terme. Si son intérêt général se réalise, alors on aura tous de nouveaux droits. Celui qui défend l’intérêt général saura en bénéficier en premier. L’intérêt général permet de créer tous les jours en tant que salarié, d’être rémunéré pour des médias créés. Seulement, ça n’est pas le cas actuellement. Donc il s’agit de créer une monnaie publique qui permette de protéger nos industries. Comme cela, cet intérêt général permettra de créer tous les jours. === Mes Notes === Écrire sur l’intérêt général et les industries. == On me Dit que je Suis Moral == Vendredi 26 novembre 2021 Être moral ça peut être bien se on prend en compte la morale de l’autre. Il ne faut pas se sentir à part mais savoir que tout le monde peut être moral. Nous avons tous une place dans la société, même ceux qui peuvent être immoraux. Peut-être que votre métier est moral. Seulement, la société est-elle morale ? Beaucoup voudraient que la morale soit active dans la société. Seulement, la morale nécessite une vision à long terme dans la société. Cette vision à long terme vous permet d’organiser votre entourage par l’éthique, une autre façon d’améliorer la morale. === Mes Notes === Écrire sur la société que vous voulez organiser à long terme. == L’Espérance == mercredi 6 octobre 2021 [[Image:LDes Coudres-Magdalene.jpg|alt=|droite|vignette|upright|<center>Sainte Marie Madeleine</center>]] L’espérance c’est se dire qu’il y aura au moins une bonne nouvelle dans la journée pour pouvoir faire le bilan de la journée le soir. L’espérance c’est aussi participer à la construction du monde ou de l’église. L’humain est fait pour construire et exécuter des tâches de construction. Le diable veut que l’on soit négatif. Il ajoute mondialement de fausses informations sur une déchéance humaine. Il s’agit de trouver ceux qui contredisent ces mensonges, de trouver une raison scientifique et historique à la vérité, celle qui nous donne espérance. Alors on communique notre espérance avec cette vérité pour défaire le plan du diable. Alors on est capable d’accepter la souffrance de son corps. === Mes Notes === Écrire sur son esprit positif. == Aller se confesser == Lundi 4 octobre 2021 [[Image:Artgate Fondazione Cariplo - Molteni Giuseppe, La confessione.jpg|alt=|droite|vignette|upright|<center>Se confesser</center>]] On cherche à se confesser pour savoir où on en est. Se confesser ne vous sera jamais reproché car le secret sera gardé. Par contre il s’agit d’aller se confesser à une autre paroisse parce que le jugement est malheureusement humain. On se confesse pour ne pas refaire les péchés qu’on a dit. Il s’agit de chercher le chemin qu’on nous a proposé. Si notre péché est grand, on cherche un objectif à long terme pour remplacer notre péché. On se confesse alors pour montrer l’évolution de sa démarche. === Mes Notes === Écrire sur ses remords. == Je Crois aux Lois == Samedi 6 novembre 2021 [[Image:Moisés, Domingo Martínez.jpg|alt=|droite|vignette|upright|<center>Les lois sont dépassées par l'éthique.</center>]] Si vous croyez en la loi sachez que la constitution est la première à ne pas être respectée par certaines lois. Aussi la constitution est alors défaite pour créer la tyrannie. Dans une période de récession les lois servent à défaire les libertés par la peur. Si vous croyez en la peur d’un politicien, sachez qu’il n’y a pas à avoir peur dans un monde ordonné. Dans les faits, ceux qui croient en la loi ne sont pas éthiques, parce que l’éthique va au-delà des lois. Dans une école Freinet, des jugements sont établis par l’éthique pour refaire le règlement intérieur. Donc croire en l’éthique, c’est à dire en quelque chose de supérieur à la loi est préférable. === Mes Notes === Écrire sur son éthique. == Je suis Satisfait == mercredi 20 octobre 2021 La satisfaction d’être humain et d’appartenir à l’église est à appréhender. Être satisfait de soi alors que nous ne sommes pas encore des anges est étrange. Nous ne sommes pas encore quelqu’un d’abouti et nous sommes satisfaits de nous. On peut donc être satisfait d’une partie de soi, mais pas de soi en particulier. Quelqu’un de satisfait sur tout ce qui le fait est irrégulier. L’humain n’est pas parfait. Nous nous sommes spécialisés. Être insatisfait permet en réalité de demander plus à Dieu parce que nous sommes imparfaits. Avoir envie de progresser est donc mieux qu’être satisfait de soi. === Mes Notes === Écrire sur ce qui ne vous plaît pas. == Je doute == samedi 6 novembre 2021 [[Image:De ongelovige Thomas Rijksmuseum SK-A-3908.jpeg|alt=|droite|vignette|upright|<center>Ter Brugghen – Thomas doute.</center>]] Reconnaître qu’on est dans le doute c’est reconnaître qu’on ne sait pas tout. On pose les questions où l’on souhaite des réponses sur papier, puis on regroupe ses questions en thèmes, puis on les fait lire par un prêtre. Le prêtre croit en Dieu par la foi en général, mais on peut croire en Dieu par la science. Donc, si ses questions sont scientifiques, on cherche de soi-même les réponses dans des conférences scientifiques créées par des croyants. La finance tend à censurer la partie sociale. Alors on croit les films libéraux, faisant croire que l’individu fait la société, alors que c’est la société qui fait l’individu. Par contre, en période de protectionnisme, les individualités seront sociales. Quand on est dans le doute, on s’intéresse à la société. On agit pour la société. === Mes Notes === Écrire sur ses doutes. == Je Suis dans les Ténèbres == Lundi 4 octobre 2021 [[Image:Emil Brack - The Discussion.jpg|alt=|droite|vignette|upright|<center>Émil Brack – La Discussion</center>]] Il s’agit de se protéger. Être dans l’esprit c’est mieux qu’être dans les émotions. Donc il s’agit de se protéger de ceux qui sont dans les émotions. Ils peuvent devenir malsain facilement. Peu d’amis osent dire la vérité sur vous. Ce sont plutôt des copains. Alors créez votre ami fidèle en étant éthique et moral avec lui. Osez lui dire ce qui peut lui être reproché. Alors on trouve la lumière. Un ami qui n’est pas dans le secret avec son péché n’est pas un ami. Son ami peut hésiter à dire la vérité. Il faut apprendre à dire la vérité à son ami pour qu’il aille vers la vérité. On peut pousser à dire la vérité en cherchant vérité dans la confiance avec son ami. On est franc alors avec son ami. Il faut d’abord supprimer les clichés sur la vérité avec son ami. Par exemple, la vérité est bonne à dire mais elle fait mal. Il ne s’agit pas d’aller contre les autres. Ce sont les personnes malsaines qui font ça. On cherche pourquoi on pense contre et on évite ce qui nous met dans cet état. Alors on trouve la lumière de Dieu avec son ami, celle qui défait nos ténèbres. La lumière qui nous mène n’est pas celle des ténèbres. On se créera un fondement qui nous renforcera. === Mes Notes === Chercher à se sortir des ténèbres en se créant un ami. == Je Suis Radin == Dimanche 7 novembre 2021 [[Image:Charles Dullin, L'Avare, 1944.jpg|alt=|droite|vignette|upright|<center>Charles Dullin en 1944, jouant L'Avare de Molière.</center>]] Être radin n’est pas un comportement utile dans l’univers de Dieu. Dieu veut que nos fruits servent à quelque chose. Dieu veut que les dirigeants servent les autres. L’économie fonctionne par le mouvement, pas par la stagnation de la monnaie. L’argent peut être vite volée. Si votre argent sert à porter des fruits pour les autres, alors vous êtes béni. Seulement, accumuler et profiter de sa situation c’est stagner. Le monde de Dieu est créé par les justes. Seulement les riches auront beaucoup de mal à atteindre le monde de Dieu. Si on est radin pour ses enfants pourquoi pas. Si on ne veut pas prendre de risques pourquoi pas. Seulement cumuler ne sert à rien dans un monde en mouvement. Donc oser prendre des risques est nécessaire au bout d’un moment. === Mes Notes === Écrire sur le mouvement de la vie. = Chercher sa Vérité = En cherchant la vérité on cherche surtout sa vérité avec les autres. == Je ne Valide pas la Bible == mercredi 6 octobre 2021 [[Image:Adolphe-monticelli-ladies-in-elegant-dress-in-a-park.jpg|alt=|droite|vignette|upright|<center>Adolphe Monticelli – On pense que les femmes construisent.</center>]] Tout ne vient pas de Dieu dans la Bible. La Génèse a été écrite 700 ans après la mort de Jésus Christ. L’histoire de Moïse ne tient pas debout. La Génèse dit cependant beaucoup de vérités. Mais elle omet la Maât des africains. La Maât des africains ordonne le monde des humains. Nous avons nous l’Esprit Saint, Jésus et Dieu. Seulement la Maât chez les africains c’est la femme. Donc les africains honorent la femme. Effectivement, si on n’écoute pas celle qui met au monde les enfants, on va vers la guerre. Discutez donc de ce que vous ne validez pas dans la Bible. Seulement, la Maât est peut-être encore plus forte que des écrits de la Bible. Lisez les livres de la librairie Tamery Sematawy Maât. === Mes Notes === Écrire sur sa maman ou sa femme. == Traverser une Épreuve == Mardi 5 octobre 2021 [[Image:Christ Falling on the Way to Calvary - Raphael.jpg|alt=|droite|vignette|upright|<center>Christ tombant - Raphaël.</center>]] Les échecs permettent d’éveiller les sens. On se dit qu’on sera plus fort après cette épreuve. Les sens éveillés permettent d’être plus conscient de l’épreuve. Seulement ils peuvent déstabiliser parce qu’on n’est pas habitué à l’épreuve. Il s’agit de dialoguer avec ses amis pour trouver un chemin qui permette d’affronter cette épreuve. L’épreuve ne s’oubliera pas. On écrit donc sur cette épreuve. Plus tard, on pourra reprendre les écrits pour mieux anticiper. On peut s’en prendre à Dieu. Seulement, l’esprit n’aide-t-il pas lorsque nous allons bien ? Soit on renforcera sa croyance, soit on la fuira. Seulement nous ne pouvons pas fuir l’épreuve. Il ne s’agit pas de comparer sa situation avec une meilleure situation. Il s’agit de comprendre cette situation pour mieux la confronter et la réparer. Il s’agit d’affronter l’épreuve, pas de la laisser passer. On ne se laisse pas écraser. Il s’agit d’être honnête avec soi-même et avec les autres. Si on pense que c’est une injustice, crier à l’injustice avec un ami de confiance. Attention, un juge se sert de tout ce qu’on dit. Il s’agit de préparer sa parole face à un juge, de répéter cette parole avec ses amis. L’honnêteté doit prévaloir sans aller trop loin dans les paroles. Il s’agit d’agir avec discernement. On écrit les solutions à son épreuve. On les compare et on en parle avec ses amis. On peut se dire que le monde nous dépasse, que nous ne sommes qu’un être qui subit une épreuve qui passera sûrement un jour si on est juste. === Mes Notes === Écrire sur un échec en essayant d’améliorer ce passage. == Ma Famille n’est pas Religieuse == jeudi 14 octobre 2021 Difficile de convaincre en dehors de sa famille si celle-ci n’adhère pas à sa religion. Cherchons cependant ceux qui écoutent le mieux notre parole. C’est dans l’adversité qu’on arrive le mieux à convaincre. Seulement nous aurons besoin de réconfort. [[Image:William-Adolphe Bouguereau (1825-1905) - Song of the Angels (1881).jpg|alt=|droite|vignette|upright|<center>William Bouguereau – Le son des anges</center>]] Si on s’est marié avec la mauvaise personne et qu’elle n’adhère pas à sa religion, alors on donne de la confiance et on montre son éthique, sa morale. Si son conjoint est d’une autre religion, alors il s’agit d’échanger. C’est beau d’échanger entre religions. On cherche l’universalité de la croyance dans un Dieu et de la vie éternelle. LirePourquoi un Dieu ? Si son conjoint est athée, il s’agit de se comprendre soi. Si on a choisi un conjoint athée, c’est qu’on était perdu. On aura du mal à convaincre avec un conjoint qui refoule sa religion. Donc, on comprend pourquoi son conjoint refoule la religion. Son conjoint est peut-être chrétien sans le savoir. Alors on loue sa démarche si elle est chrétienne. On se réconforte si on pense que son conjoint va rejoindre Dieu. On cherche à donner la foi à son conjoint s’il en a besoin. Son conjoint doit accepter notre religion. Il ne doit pas la refouler. Donc on se met d’accord sur la frontière des débats religieux et athées. La morale doit être respectée au mieux par l’éthique, c’est à dire aller au devant de la morale. On est encore plus éthique avec un conjoint athée. === Mes Notes === Écrire sur ceux qui ne croient pas en un Dieu. == Chercher sa Vérité == lundi 27 septembre 2021 Avoir la foi demande un chemin de vérité. Tant qu’on n’a pas réponse à nos questions, nos questions peuvent revenir, que ce soit sous forme d’affirmations ou de questions. Ce sont les scientifiques militants qui peuvent répondre à beaucoup de questions. Seulement la réponse peut ne pas plaire. Au début, on ne trouve pas immédiatement les réponses aux questions qu’on nous pose. On se dit alors qu’on trouvera réponses en cherchant sa vérité. Seulement il faut aussi chercher la vérité avec les autres pour trouver sa vérité. [[Image:Friant La Discussion politique.jpg|alt=|droite|vignette|upright|<center>La discussion politique – Émile Friant</center>]] Ce sont les autres qui permettent de nous connaître donc travailler sa communication avec les autres est important, surtout avec l’école qui nous empêche souvent de communiquer en restant à écouter un professeur. La vérité des autres fait plaisir en général. Mais sa vérité peut faire peur ou faire mal. La vérité gêne l’humain en général. Quand son égoïsme se révèle, c’est peut-être qu’une vérité sur soi gêne. Méditer permet de grandir avec une vérité qui nous a gêné. Quand on a découvert sa vérité, on a pris peur. On aimerait ne pas voir cette vérité, l’oublier, constater que les autres n’en parlent pas. Puis on refoule cette vérité. Puis on est humble avec soi-même en y repensant. Alors on s’ouvre aux autres. On mène une nouvelle vie d’évangile en voyant qu’on est pécheur. On est libre et on a peur d'être libre. On trouve alors de nouveaux repères. Certains peuvent prendre une mauvaise voie à ce moment, ne pas vouloir être libre. Alors, si on veut rester libre de sa vérité, les autres nous disent qu'on a changé. On aimerait que les autres voient leur vérité. Donc on milite pour comprendre les autres afin de leur apporter leur vérité, la vérité douce, celle qui est proche de nous. Puis on trouve la vérité qui fait l'humain à un moment donné ou d’autres vérités sur soi. On l'aurait vue autrement sans savoir sa vérité. Ça peut être une vérité dite par un prophète, une vérité que l'on trouve, ou une nouvelle vérité sur soi qu’on accepte bien cette fois-ci. Alors on a envie que les autres comprennent cette vérité qu'ils ne voient pas. Il s’agit de comprendre qu’on n’y croyait pas et qu’on y a cru par un chemin. Il s’agit de demander si on gêne l’autre quand on pense à sa vérité. On pense pourtant qu’on est dans la vérité alors qu’on est dans sa vérité. Les pauvres nous donnent la situation du monde. Nos amis connaissent nos défauts et nous donnent en secret les efforts à faire pour avancer. On peut avoir des copains au travail, rarement des amis. === Mes Notes === Écrire sur ce que l’on ne sait pas. = Chercher la Vérité = Être dans la vérité peut faire de nous un prophète. C’est pour cela qu’on cherche sa vérité et la vérité avec son cœur. On franchit des étapes de vérités sur soi-même et les autres. On médite puis on accepte la vérité envisagée. Alors on grandit et on assimile la vérité. Puis on pourra atteindre la vérité du monde à un instant donné. == Lire la Bible == samedi 16 octobre 2021 [[Image:Amile-Ursule Guillebaud - Interior with elderly woman reading the Bible.jpg|alt=|droite|vignette|upright|<center>Amile-Ursule Guillebaud – Lire la Bible.</center>]] Il existe la catéchèse ou les parcours Alpha pour lire la Bible. Contactez votre paroisse pour rejoindre votre groupe de lecture. La Bible ne se comprend pas entièrement. Elle signifie beaucoup de choses en même temps. Lire seul la Bible se fait après avoir suivi un groupe de lecture. L’ancien testament raconte une période de l’humain qui est passée. Dieu ne se défait plus des humains qui ne le suivent pas. Le nouveau testament est actuel mais reprend une partie de l’ancien testament pour le confirmer. Si vous voulez lire la Bible sans groupe de lecture, commencez par le nouveau testament, puis lisez l’ancien testament en sachant que c’est une période passée. === Mes Notes === Écrire sur ce qui vous a plu dans le nouveau testament. == Avec les Malades == vendredi 5 novembre 2021 [[Image:PeinturesMuséeFabre048.jpg|alt=|droite|vignette|upright|<center>Pierre Dulin – Les malades veulent guérir.</center>]] Les malades ont une toute autre vision de la vie. Ils la voient comme cruciale et fragile. Ceux qui ont été tout le temps malades ont donc une vie extraordinaire à comprendre. On se prépare alors à voir ce qu’on ne pensait pas voir. Seulement, les plaintes du manque de considération peuvent jaillir. Alors on se confesse avec le malade pour le révéler à sa vie extraordinaire. Peut-être s’ouvrira-t-il à nous. L’art de se détacher du monde est alors à prendre en compte. Profiter du moment présent est alors à envisager. En réalité, les malades sont comme vous et moi. Ils ont des habitudes qui ne sont pas à la mode. Seulement, ils ont une meilleure appréhension de la vie parce qu’ils observent. === Mes Notes === Écrire sur un moment de maladie. == Devenir Libre == Lundi 27 septembre 2021 Devenir Libre est un désir d’autonomie. Mais devenir libre fait peur. Pour ne pas changer, beaucoup ne veulent pas devenir libre. [[Image:Man Writing a Letter by Gabriël Metsu.jpg|alt=|droite|vignette|upright|<center>Écrire des poèmes rend libre – Gabriel Metsu.</center>]] Chercher la vérité permet de devenir libre. C’est sortir du monde du diable qui fait qu’on devient libre. On se crée un fondement qui nous renforce. On pose les questions qui piègent aux autres, ceux qui savent, le non respect des dogmes de la Bible par exemple, puis on donne les réponses une fois qu’on a trouvé les réponses. On aura été habitué au monde du diable. Donc on voudra inconsciemment y revenir au début de sa liberté. On réfléchit sur la morale par l’éthique. On trouve des réponses à nos questions avec son ou ses amis. On a de quoi devenir responsable de projets, comme connaître la communication et la prise de responsabilités. Si on a une vie de tentations contre la morale, on essaye d’abord de s’en détacher, puis on part à l’aventure d’une nouvelle vie. Il s’agit de choisir entre une vie carriériste ou une vie éthique. Puis on voit les faiblesses avec le monde du diable. On est né de nouveau. Être libre permet alors de devenir heureux avec sa véritable condition, celle qui fait qu’on sortira de son corps, après avoir trouvé plein de fois la vérité. On aura donc été prophète. La mort sera un passage. Jésus peut nous choisir si notre poutre est suffisamment solide. Appeler à l’aide est un signe de liberté. Notre cœur veut devenir libre. Alors on essaie de trouver la bonne personne pour nous aider. === Mes Notes === Vous sentez-vous libre ? == Le Chemin de Vérité == vendredi 24 septembre 2021 Le chemin de vérité est un chemin de progressions et de régressions pour comprendre le monde dans lequel on est. L’aboutissement du chemin est de devenir prophète. Savoir mener sa vie est primordial pour trouver le bon chemin. Il faut savoir se protéger des mauvais chemins pour trouver la vérité. Il faut oser trouver le bon chemin, celui qui dit la vérité de la Bible. Son chemin de vérité permettra de grandir. Au début, nous agissons tous comme des enfants. L’enfance revient même quand on est adulte parce que notre esprit veut rester dans le bonheur de l’enfance. [[Image:Mirabello Cavalori (1535-1572) - A Discussion - NG3941 - National Gallery.jpg|alt=|droite|vignette|upright|<center>Mirabello Cavalori - Une Discussion</center>]] L’hypothèse supérieure qui englobe un ensemble d’idées et qui les défait toutes est difficile à atteindre dans un monde cartésien. Il s’agit de lire Platon, Lyndon Larouche, De Cuse, Leibniz, Riemann, Einstein ou des livres scientifiques de la librairie Tamery Sematawy Maât. C’est une hypothèse supérieure qui nous fera grandir. Nous répondrons à notre vérité par une nouvelle vérité qui fera de nous un adulte. Cela fera peur. On trouvera de nouveaux repères. L’esprit est là pour nous aider, que l’on soit méchant ou gentil. L’esprit veut que nous cherchions la vérité pour révéler une partie de soi et des autres. L’esprit aime les amis qui nous permettent de trouver vérité. N’avons-nous jamais vu des décideurs chercher des conseillers ? Les conseillers sont eux-mêmes conseillés et font le bien ou le mal avec. Ainsi, l’esprit veut nous révéler la vérité, celle du mal si nous cherchons la vérité du mal. Seulement il coupe la vérité aux mauvais décideurs et à leurs conseillers. C’est pour cela que des décideurs se mettent à faire n’importe quoi. Quand on ne cherche rien l’esprit ne nous aide pas. Les chrétiens sont ceux qui cherchent vérité. En effet, nous sommes baptisés pour trouver le Christ et Christ est vérité. Seulement il y a des chrétiens païens qui cherchent à faire le bien parce que cela fait du bien. Les chrétiens non païens ont en général une bonne place dans la société parce qu’apporter du bien a permis aux parents d’être bien vus. Pourquoi voulons-nous le mal ? Parce que le mal existe. Il ne permet pas la vie en société donc Dieu se passe de ceux qui cherchent le mal ou vivre l’instant présent sans apporter aux autres. Par contre si nous voulons savoir pourquoi il y a le mal et le bien, nous trouvons réponses, parce qu’il y a forcément quelqu’un qui a créé tout ça. Dieu ne s’intéresse pas aux biens matériels. Ainsi, vous ne trouverez pas vérité avec des biens matériels que vous avez consommés. Notre esprit est en effet vite diverti par ce que nous lisons. Notre esprit est encore animal et ne voit pas facilement l’envers du décor, parce que l’esprit est fait pour savoir si nous pouvons aimer par la vérité, pas après la vérité. C’est la vérité qui libère. Au début on a peur, puis ensuite on veut en savoir plus. Seulement il s’agit de passer cette peur, de découvrir sa vérité pour chercher la vérité. === Mes Notes === Chercher sa vérité pour aller chercher la vérité. Ceci est une démarche quotidienne. == Convaincre par la Politique == Samedi 25 septembre 2021 Les chrétiens sont souvent persécutés pour leur recherche de vérité. En occident ils ont fait face en 2021 à l’individualisme forcené qui aboutit à tout vouloir faire par soi-même. Un chrétien a des amis qui lui permettent d’avancer dans la démarche de recherche de vérité et lui permettant d’aimer. [[Image:Solidarité et progrès Paris 2015.jpg|alt=|droite|vignette|upright|<center>Des partis politiques militent dans les rues.</center>]] Convaincre avec l’évangile ça peut être s’intéresser à une partie de l’évangile qui est faussée par le discours ambiant. Si une partie de l’évangile semble fausse, alors c’est toute la Bible qui est remise en cause par les païens. Ils vont alors vous embêter avec ce qu’ils trouvent incertain. Mais ils vont vous dire par exemple : « Pourquoi tant de précarité s’il faut se multiplier ? ». En période de récession cette partie de l’évangile est remise en cause alors qu’elle est vraie. On peut toujours se multiplier. L’esprit et la nature permettent cela. Ce qui gêne c’est la cupidité de l’humain. L’argent devient un but en période de récession pour survivre, alors qu’on pourrait se développer. En période de récession, il faut agir en politique pour changer ce paradigme de cupidité. Le chrétien est individualisé par les médias appartenant au diable en 2024. Les médias qui le contredisent le mieux sont diabolisés. Ainsi, il faut s’intéresser à celui qui est peu écouté. Pourquoi cela ? Si des passages de la Bible sont faussés, alors ceux qui défendent ces passages de la Bible ne sont pas écoutés. Il s’agit donc d’écouter ceux qui défendent les phrases contredites de la Bible. Si nous ne croyons pas ces personnes, alors il y a sans doute des personnes cachées qu’il s’agit de découvrir. Il n’y a pas à s’accorder quand on n’est pas d’accord avec quelqu’un. On n’est pas d’accord et chacun le sait. Une vérité permet d’oublier le désaccord. Seulement il faut comprendre ce désaccord donc ne pas l’oublier pour le résoudre. === Mes Notes === Aller voir tous les partis politiques. == J’ai Peur de Convaincre == Samedi 25 septembre 2021 [[Image:Frédéric Bazille 001.jpg|alt=|droite|vignette|upright|<center>Frédéric Bazille - Une réunion</center>]] Si vous avez peur de convaincre c’est soit que vous n’avez pas essayé, soit que vous n’êtes pas assez convaincu, soit que vous n’aimez pas. La recherche de Dieu est perpétuelle parce que nous ne verrons le créateur que dans le monde d’après, si nous sommes prophètes sans doute. Dieu se comprend avec l’ensemble de l’univers, des lois de la physique, des lois du vivant surtout, avec la nature créative humaine, avec l’esprit aussi. On a peur d’essayer parce que l’on se dit qu’on ne convaincra pas. Nous ne sommes pas forcément faits pour la communication. Mais la communication s’apprend. Il s’agit surtout d’aimer. Tout notre univers est une création de Dieu. Lisez le livre '''Pourquoi un Dieu ?''' Pour mieux comprendre. === Mes Notes === Si vous avez peur d’essayer vous pouvez suivre un cours de communication, culture et expression au CNAM ou vous pouvez rejoindre un groupe de théâtre ou mieux, d’improvisation. Chercher Dieu avec les autres. = Être dans la Vérité = Être dans la vérité c’est aussi utiliser son amour. On souhaite que les autres aillent dans le monde de Dieu. Être dans la vérité c’est se réjouir d’avoir une nouvelle vérité humaine, sans penser à la sienne mais en voulant que chacun y aille. On est dans la vérité quand on transmet la Bible, quand on trouve ce qui fait l’humain à un instant donné. Alors on convainc les autres de créer le monde que veut Dieu pour nous. Il est peut-être déjà là sans qu’on le voit == Lire la Bible à un Athée == Lundi 4 octobre 2021 La catéchèse permet aux volontaires d’apprendre la Bible. Il y a aussi les livrels des témoins de Jéhovah qui permettent d’être positif avec la Bible. Lire la Bible avec un adulte athée nécessite de bien connaître la Bible, parce que ce qui va intéresser l’athée peut évoluer dans le temps. [[Image:Jean-Baptiste Greuze - Reading the bible.jpg|alt=|droite|vignette|upright|<center>Lire la Bible c’est mieux à plusieurs.</center>]] Quand quelqu’un donne un péché et que l’on ne l’a pas fait on peut penser qu’il ne va pas aller voir Dieu. Cependant Dieu donne sa miséricorde à quiconque. Il s’agit donc de dire que Dieu pardonne parce que dire son péché c’est un peu se confesser. Il faut aussi se protéger et partir si on est touché. Après on pourra réagir correctement à ce genre de péché. Dans un monde étriqué, il s’agit de faire de la psychologie pour que les personnes qui sont en face de nous se dévoilent. Alors la Bible intervient pour aider. On veut tout savoir. Seulement dire un péché c’est un peut se confesser. Donc ses désirs humains sont à laisser de côté avec le péché. C’est le cœur qui doit parler à ce moment là. Si celui qui dit son péché n’accepte pas le pardon, alors on peut se demander comment il peut rejoindre Dieu. Il s’agit de construire, pas de détruire. Il s’agit de dire ce qu’on ressent, pas de cacher la miséricorde. Sinon vous serez jugé de la même manière que vous jugerez. La Bible doit pouvoir être interprétée avec amour. Elle doit servir à aider, pas à reprocher. Si on voit celui qu’on veut convaincre comme quelqu’un de malsain, alors on peut chercher pour lui la miséricorde ou le bonheur. Il s’agit de lui montrer notre amour, pas de le réprimer. Il s’agit de deviner à qui on a affaire. Si la personne dit des choses malsaines, elle est peut-être honnête avec elle-même. Alors il s’agit de donner des conseils. On ne doit pas accepter ce qui est malsain. Il s’agit de partir quand on se sent influencé ou de trouver ce qui est positif pour couper les paroles malsaines. === Mes Notes === Lire la Bible quand on peut s’en souvenir. == La Bonne Nouvelle, L’Évangile == Samedi 9 octobre 2021 [[Image:Leonardo da Vinci (1452-1519) - The Last Supper (1495-1498).jpg|alt=|droite|vignette|upright|<center>Léonard de Vinci, le dernier dîner</center>]] L’évangile nous appelle à quelque chose de nouveau. Donc elle montre qu’on s’adapte aux autres pour la bonne nouvelle, qu’il y a une vie après la mort. Si on ne croit pas qu’il y a la vie après la mort, on peut regarder les témoignages de mort imminente, où des personnes quittent leur corps et voient différemment les choses ensuite. Si on ne croit pas à la vie après la mort, on est peut-être dans un individualisme qui nous empêche de voir les autres. La vie est un miracle. Notre individualisme conforté par le libéralisme nous empêche de voir cela. Alors on s’intéresse aux films sociaux peu nombreux. On s’intéresse aussi aux maîtres qui ont combattu le libéralisme pour permettre des booms économiques, maîtres cités dans les autres livrels. La Bonne Nouvelle est un objectif à comprendre. On ne comprend pas tout dans la bonne nouvelle tout de suite. Jésus disait beaucoup de choses dans ses paroles. C’est pourquoi il s’agit de parler de sa bonne nouvelle, de cette bonne nouvelle qui nous mène. On passera par des épreuves, dirigées par le diable certaines fois. On peut éviter certaines épreuves. Il faut chercher une sincérité, c’est à dire une compréhension de soi et des autres avec ces épreuves. === Mes Notes === Écrire sur les paroles de Jésus. == Révéler son Chemin == Samedi 13 novembre 2021 [[Image:Norman Rockwell - Fishing Trip, They'll Be Coming Back Next Week - Google Art Project.jpg|alt=|droite|vignette|upright|<center>Son chemin est celui des autres - Norman Rockwell.</center>]] Votre démarche de recherche de vérité est importante. Ceux qui discutent avec vous veulent surtout savoir pourquoi vous croyez en Dieu. Ainsi votre chemin est à donner aux autres. Cela peut même donner un chemin à suivre aux autres. On a à faire des choix pour plus tard et vous donnez un chemin possible. Donc n’hésitez pas à montrer votre chemin de vérité. Seulement, la foi est encore plus importante pour transmettre. Donc ce chemin de vérité a fini par donner la foi. Les étapes qui vous ont permis de croire en Dieu sont cependant importantes. Ne pas avoir cru en Dieu est peut-être une étape à donner. === Mes Notes === Écrire sur son chemin de vérité sur Dieu. == Devenir Saint == Lundi 27 septembre 2021 [[Image:Église Saint-Martin de Castelnau-d'Estrétefonds - La Prédication de saint Jean-Baptiste par Robert Arsène IM31000072.jpg|alt=|droite|vignette|upright|<center>La Prédication de saint Jean-Baptiste - Robert Arsène</center>]] Dieu seul est saint. Seulement nous sommes appelés à devenir saint. Si nous le sommes un instant, c’est déjà un grand chemin de parcouru. Pour devenir saint à certains instants, on cherche la vérité avec les autres pour agir avec les autres pour un monde nouveau, le monde que veut la Bible. Alors on devient prophète ou saint un instant. On dit aussi qu’on est saint quand on est accepté dans le monde de Dieu. En réalité on est juste. Le monde de Dieu est très accessible en réalité. C’est plutôt que l’humain a tendance à suivre le diable parce qu’enfermé dans son monde. C’est pour cela qu’il faut parler du monde de Dieu. Un juste devrait pouvoir parler de lui-même de la façon que Dieu le voit. On écrit des poèmes. === Mes Notes === Se sent-on juste ? == Porter sa Croix == Dimanche 26 septembre 2021 Porter sa croix permet de défaire son orgueil. En effet, ne connaître que la bienveillance fait de nous un roi qui veut tout, un enfant gâté en quelque sorte. Dire la vérité embête les autres. Ils ne peuvent pas accepter d’être dans le mensonge. Ainsi ils refoulent ceux qui disent la vérité en fonction de clichés ou de fausses raisons. Beaucoup sont dans le mensonge en période de récession. Donc les chrétiens qui disent la vérité, celle qui permet d’avancer surtout, sont refoulés. Puis leur parole agit sur certains. Pour porter sa croix il faut avoir vu la vérité et avoir été convaincu par cette vérité qui englobe le monde à un instant donné. Alors on répète cette vérité qui fait avancer le monde pour se voir rabaissé par des personnes dans le mensonge. Alors l’esprit et les conseils de son entourage nous aident à trouver la bonne voie. [[Image:Augustins - Le Christ en croix avec l'orant du cardinal Guilhem Peire Godin - 49 6 15.jpg|alt=|droite|vignette|upright|<center>La croix rappelle un chemin à suivre.</center>]] Il s’agit donc de connaître ceux qui sont dans le mensonge pour pouvoir apporter un chemin qui permette de sortir du mensonge. Le mensonge c’est vouloir continuer à vivre au jour le jour en pensant qu’on ne vit qu’une fois. Tout va pour cela dans une période de récession. Il s’agit de montrer l’intérêt général qui permet de nouveaux droits. L’intérêt général nécessite une éthique personnelle. Ainsi on voit sur le long terme pour se créer de nouveaux droits. Pour aller contre le mensonge qui dit que l’humain est mauvais, il s’agit de montrer sa bonté et d’expliquer pourquoi un comportement inadapté se passe. Par exemple beaucoup jettent les déchets dans la poubelle. Seulement nos états envoient des déchets en Afrique. L’humain est bon quand il naît mais une mauvaise société le rend mauvais. Aller contre la société est dangereux. Donc il s’agit de trouver un groupement politique qui aille vers la société idéale. Ces groupements politiques existent. Mais ils sont minoritaires en période de récession. La société idéale est morale et éthique. Donc il s’agit de trouver des personnes morales et éthiques. Porter sa croix doit servir à quelque chose, parce que l’on sera refoulé. Porter sa croix nécessite d’être dans la communication qu’on utilise. On aura développé son intelligence inter-personnelle en cherchant la vérité avant de porter sa croix avec ses amis. Porter sa croix c’est dire la vérité sans qu’elle soit accomplie. Par contre on est soi-même accompli. Si on est un prophète, cette vérité s’accomplira plus tard. Alors on trouve quelqu’un qui écoute. On aura peut-être trouvé un nouvel ami. Se souviendra-t-on de celui qui écoute ou de ceux qui n’ont pas écouté ? === Mes Notes === Écrire sur la passion de Jésus Christ. == Naître de Nouveau == Dimanche 26 septembre [[Image:Lutheran baptism.jpg|alt=|droite|vignette|upright|<center>Un baptême</center>]] Ce sera souvent après un gros échec qu’on naîtra de nouveau. Ou bien on aura trouvé un philosophe qui nous aura pris aux tripes. On aura surtout eu un ami nous ouvrant l’esprit. Quand on naît de nouveau, on voit toute la mauvaise société comme un gigantesque péché. On n’accepte plus la mauvaise voie et on la voit en fonction de ses capacités. On est alors dégoûté par la mauvaise société. Il s’agit alors de construire la bonne société avec ses amis. Il s’agit de trouver ce qui permettra la nouvelle société. Elle est là avec les gens qui sont autour de nous. Puis on s’adapte à cette mauvaise société. Donc on connaît cette mauvaise société. Seulement notre éthique reste là parce que nous avons découvert la vraie société. On convainc d’une nouvelle société. On pourrait écrire de beaux poèmes sur sa nouvelle naissance. Les poèmes auront déjà permis de voir nos défauts. === Mes Notes === Écrire sur son adolescence. == Prêcher l’Évangile == mardi 5 octobre 2021 On prêche l’évangile pour mieux l’assimiler. Seulement on sait déjà l’évangile. On ne sait pas par contre à quels points elle est vraie. L’ancien testament est plus douloureux que le nouveau testament. Être dans le monde de Dieu est possible. Donc prêchons le nouveau testament. Après avoir milité ou prêché on sera dans la vérité si on essaye de convaincre les autres. Donc cherchons la vérité avec les autres. La semence de sa parole porte des fruits si nous unissons l’esprit avec lequel nous parlons. Beaucoup sont divisés. === Mes Notes === Écrire sur le nouveau testament. == Sur Internet == Mercredi 6 octobre 2021 Internet c’est un va-et-vient. Regarder les directs permet de souvent commenter. Il ne faut pas hésiter à commenter les directs scientifiques pour montrer la science de l’église. Les directs politiques demandent souvent de l’espérance. Donner ses bonnes nouvelles qui contredisent le diable permet de faire réfléchir. Créer une carte de visite avec des références gratuites de vidéos ou de livrels gratuits permet de donner de l’espérance. === Mes Notes === S’intéresser aux vlogs scientifiques et politiques. = Prophétiser = Deviner l’avenir permet de prophétiser. Ainsi, connaître l’économie réelle permet de prophétiser. == Révéler Dieu Scientifiquement == mercredi 6 octobre [[Image:Hubble Sees a Bizarre Cosmic Rarity (11241869426).jpg|alt=|droite|vignette|upright|<center>Une rareté cosmique</center>]] Il ne s’agit pas de parler directement de Dieu, mais de dire que l’on vient de Dieu et comment. Dieu serait la première vie et Dieu nous a créé créatifs comme lui pour parler avec des amis, les justes. Les âmes ne se voient pas dans notre monde. Pourtant, on peut les deviner parce que la matière ne peut pas s’organiser toute seule. Il faut une intervention d’un autre univers pour que la matière s’ordonne. Jésus est le sauveur. Les preuves des paroles de Jésus sont incontestables. Je pense qu’on sera avec Jésus Christ après l’apocalypse. Qu’en pensez-vous ? === Mes Notes === Lisez '''L’Univers est Vivant !''' == Être dans l’Esprit == Jeudi 14 octobre 2021 [[Image:Quirijn van Brekelenkam - Interior with Cardplayers.jpg|alt=|droite|vignette|upright|<center>Une discussion - Quirijn van Brekelenkam</center>]] Pour être dans l’esprit, il faut avoir la foi. Des athées peuvent avoir la foi et suivre l’esprit, parler avec son intuition en allant de l’avant. L’autre nous dit quelque chose et on parle avec l’esprit par rapport à ce qu’on pense de lui. On ne sait pas ce qu’on va dire à l’instant, mais l’esprit nous mène pour parler à l’autre. Si on est dans l’esprit saint, on est subjugué par ce qu’on a dit. Puis, plus tard, on se demande comment on a pu dire ça. Ça peut aussi être une hypothèse supérieure qui vient quand on réfléchit, c’est à dire quand on confronte les idées dans la tête en méditant. Alors cette idée défait certaines de nos idées pour les refaire sous une autre forme plus tard. Si on n’a pas la foi, on n’arrive pas à être dans l’esprit. C’est après avoir répondu à ses questions, sa recherche de vérité, qu’on peut parler dans l’esprit. Comme c’est beau de garder l’enfance de son premier amour pour Dieu. Garder la fraîcheur le plus longtemps possible est important. Après on sera sage. === Mes Notes === Essayer d’être mené par l’esprit quand on parle. == Devenir Prophète == mardi 28 septembre 2021 [[Image:Giorgio Vasari - The Prophet Elisha - WGA24289.jpg|alt=|droite|vignette|upright|<center>Le prophète Élie - Giorgio Vasari</center>]] Le prophète intervient quand il y a récession. Il permet de diffuser le vrai message de la Bible, celui que la finance contredit dans la Bible. Il peut ajouter de nouveaux préceptes. Pour devenir prophète, il faut avoir une intelligence inter ou intra personnelle forte. Il s’agit de savoir communiquer par écrit grâce à Internet ou par oral. Un prophète a surtout la capacité d’apprendre. Il a forcément développé ses intelligences grâce aux autres. Le prophète a surtout beaucoup de liens avec la société lui permettant de ne pas être tué. Alors le Christ peut l’aider. Si l’esprit est entièrement dépendant de votre vérité pendant plusieurs, vous ne dormez pas pendant plusieurs jours sans que cela fatigue votre corps. Aussi vous aurez toujours le bilan de l’intuition de tous les humains à 500 mètres autour de vous. Alors l’esprit, l’esprit saint ou le Christ, voire Dieu, aident le prophète à faire face au monde par la vérité du prophète englobant le monde. === Mes Notes === Essayer de militer pour convaincre les autres du monde de Dieu ou d’une économie humaine. == La Parole de Connaissance == lundi 18 octobre 2021 La parole de connaissance est une connaissance sur le plan de Dieu qui va se passer. La parole de connaissance n’est pas une idée qui passe par la tête. On est sûr dans son esprit qu’elle va se produire. La parole de connaissance n’est pas à confondre avec l’économie. On peut deviner ce qui va se passer dans la société avec l’économie. === Mes Notes === Ai-je déjà été sûr qu’un événement se produise ? == La Parole de Sagesse == mercredi 20 octobre 2021 La parole de sagesse est une révélation surnaturelle donnée par Dieu. Cela donne une nouvelle vérité sur l’humain qui arrivera peut-être plus tard, distribuée par un prophète. On ne peut distinguer facilement la parole de sagesse de la vérité scientifique qu’avec la partie surnaturelle de la parole de sagesse. C’est quelqu’un qui est dans la vérité qui reçoit la parole de sagesse. Il n’a pas à être plus intelligent que les autres pour être dans la vérité. === Mes Notes === Écrire sur le surnaturel de la Bible. {{AutoCat}} mdd9dwq797v3m8s1jphm4a0lq5zkw15 Fonctionnement d'un ordinateur/Les architectures à accumulateur 0 82386 763756 762876 2026-04-16T13:34:10Z Mewtow 31375 /* Les registres d'interruption avec un processeur hybride-accumulateur : l'exemple du Z80 */ 763756 wikitext text/x-wiki Les architectures que nous avons vu précédemment dans ce cours disposent de registres pour les données, en plus du pointeur de pile, d'un ''program counter'', et de quelques autres. Elles sont appelées des '''architectures à registres''', terme qui trahit bien le fait qu'elles ont des registres généraux ou spécialisés pour stocker temporairement des données. Et si on leur a donné un nom, c'est parce qu'il existe des architectures qui ne sont pas dans cette catégorie. Il en existe plusieurs types, mais ce chapitre va se concentrer sur les '''architectures à accumulateur'''. [[File:Isaccumulator.png|vignette|Architecture à accumulateur.]] Les architectures à accumulateur sont centrées autour d'un registre architectural appelé l''''accumulateur'''. Il est utilisé pour toutes les opérations arithmétiques dyadiques, où il sert à la fois de source et de destination. Toutes les instructions dyadiques sont de type ''load-op'' : une opérande est lue depuis l'accumulateur, le second opérande est lu depuis la mémoire RAM, le résultat de l'instruction est automatiquement mémorisé dans l'accumulateur. Les instructions monadiques peuvent utiliser un opérande dans l'accumulateur, ou dans la mémoire RAM, les deux sont théoriquement possibles. La conséquence est que le nombre d'accès mémoire est drastiquement diminué : de 3 par instructions sur une architecture mémoire-mémoire, on passe à seulement un avec un accumulateur. Les opérations dyadiques ont besoin d'un seul accès mémoire pour lire la seconde opérande, les opérations monadique en font aussi un seul pour lire leur unique opérande. {|class="wikitable" |- ! Classe d'architecture ! Nombre d'accès mémoire par opération dyadique |- |- ! Architecture à accumulateur | Un accès mémoire par instruction, pour lire la seconde opérande |- ! Architecture à registres | Zéro si les opérandes sont dans les registres, un pour les opérations ''load-op'', LOAD et STORE. |} ==Le jeu d'instruction des architectures à accumulateur sans registres d'indice== L'accumulateur est adressé grâce au mode d'adressage implicite, de même que le résultat de l'opération. Par contre, les autres opérandes sont localisés avec d'autres modes d'adressage, et lues en mémoire RAM. Le résultat ainsi qu'un des opérandes sont adressés de façon implicite car dans l'accumulateur, seule la seconde opérande étant adressée directement. Grâce à l'accumulateur, une instruction ne fait qu'un seul accès mémoire maximum, ce qui rend le processeur très facile à implémenter. ===Les registres des anciennes architectures à accumulateur=== [[File:IBM 701console.jpg|vignette|IBM 701, console extérieure.]] Les architectures à accumulateur étaient communes dans les années 50-60. A l'époque, les ordinateurs étaient des ''mainframes'', à savoir des ordinateurs gigantesques, qui occupaient une pièce de bâtiment complète dans le pire des cas, une armoire entière dans le meilleur. Les ordinateurs de l'époque étaient surtout utilisés pour du calcul scientifique ou des tâches d’ingénierie demandant beaucoup de calcul, rarement pour de la comptabilité ou des tâches administratives. En conséquence, ils devaient gérer des nombres entiers, mais ne supportaient pas de texte, ni de nombres encodés en BCD. De tels processeurs utilisaient l'adressage par mot, et non par byte, vu que ce dernier est surtout utile pour adresser des caractères de texte. Les nombres flottants n'étaient pas encore apparus. A la place, les ordinateurs de l'époque géraient des nombres entiers de grande taille, de 30 bits ou plus. En conséquence, l'accumulateur faisait facilement 30 à 60 bits. Par exemple, les ordinateurs de la Série scientifique IBM 700/7000 géraient des entiers de 36 bits, l’accumulateur faisait 38 bits : 36 bits plus deux bits pour les débordements. Historiquement, les premières architectures à accumulateur ne contenaient aucun autre registre que l'accumulateur. Ce n'est que dans les années 60 que de nombreuses architectures à accumulateur ont ajouté un second '''registre pour les multiplication/divisions'''. Un exemple est celui de l'IBM 701, qui incorporait un registre accumulateur de 38 bits et un registre multiplieur de 36 bits. Le registre mémorise le multiplieur lors d'une opération de multiplication. Il mémorise donc un opérande, pas le résultat. Il peut aussi décaler le multiplieur vers la droite/gauche, ce qui est utile pour exécuter la multiplication. C'est ce qui est fait sur l'IBM 7094. Une autre possibilité est que ce registre mémorise une partie du résultat de l'opération. Pour rappel, le résultat d'une multiplication/division est codé sur deux fois de bits que ses opérandes. Par exemple, multipliez deux opérandes de 30 bits, vous obtiendrez un résultat de 60 bits. Les 30 bits de poids faible du résultat vont dans l'accumulateur, les 30 bits de poids fort vont dans ce second registre. Les architectures à accumulateur parfois un '''registre pour le pointeur de pile''', utilisé pour gérer les fonctions/procédures. Les architectures à accumulateurs supportaient une pile d'adresse de retour, souvent une pile d'appel. Mais pour cela, il fallait mémoriser le pointeur de pile dans le processeur, ce qui demandait un registre dédié. ===L'adressage indirect avec l'accumulateur=== Les instructions LOAD et STORE existent bel et bien sur les architectures à accumulateur, mais n'ont pas les mêmes modes d'adressages. L'instruction LOAD copie une donnée de la RAM vers l'accumulateur, l'instruction STORE copie l'accumulateur dans une adresse. Les deux instructions n'ont pas besoin d'adresser l'accumulateur, qui est adressé de manière implicite, juste de préciser l'adresse à lire/écrire. Les architectures à accumulateur supportaient souvent le mode d'adressage indirect mémoire. Un exemple est celui des ordinateurs Data General Nova, qui sont des architectures à accumulateur et qui supportaient ce mode d'adressage. Les deux instructions LOAD et STORE existaient en deux versions, distinguées par un bit d'indirection. Si ce bit est à 0 dans l'opcode, alors l'instruction utilise le mode d'adressage absolu normal : l'adresse intégrée dans l'instruction est celle de la donnée. Mais s'il est à 1, alors l'adresse intégrée dans l'instruction est celle du pointeur. Cependant, la présence de l'accumulateur permettait d'utiliser l'adressage indirect à registre, pour gérer les pointeurs. Pour rappel, avec le mode d'adressage indirect à registre, l'adresse à lire/écrire est dans un registre. Ici, l'adresse à lire/écrire est prise dans l'accumulateur, seul registre disponible pour. L'adressage indirect est plus simple à implémenter pour l'instruction LOAD, car elle prend un seul opérande : l'adresse à lire. L'adresse à lire est placée dans l'accumulateur, la donnée lue est elle aussi chargée dans l'accumulateur. Pour l'instruction STORE, il faut fournir deux opérandes, l'adresse et la donnée à écrire, ce qui peut poser problème. Mais au pire, il est toujours possible d'utiliser l'adressage absolu ou indirect mémoire : l'adresse est dans l'instruction, la donnée à écrire dans l'accumulateur. ===L'encodage des instructions=== Sur une architecture à accumulateur l'encodage d'une instruction dyadique est assez simple, vu que l'accumulateur est adressé implicitement. La seconde opérande est localisée soit par une adresse mémoire (adressage absolu), soit est une constante (adressage immédiat). L'adresse mémoire est généralement assez longue, plus que l'opcode. {|class="wikitable" |+ Encodage d'une instruction dyadique |- ! rowspan="2" | Opcode | Adresse mémoire (adressage absolu) |- | Constante (adressage immédiat) |} L'encodage d'une instruction monadique est similaire. Si l'opérande est lue depuis la mémoire RAM, alors l'instruction est encodée comme une instruction dyadique. Il en est de même pour les instructions qui copient une constante dans l'accumulateur. Par contre, si l'opérande est dans l'accumulateur, alors il y a juste besoin d'encoder l'opcode. La majorité des instructions a besoin de préciser une adresse, rares sont celles qui s'en passent. Au vu de cet encodage, les architectures à accumulateur sont qualifiées d''''architectures à une adresse''' par abus de langage. Il est intéressant de contraster leur encodage avec les architectures à registres. Les architectures à registre doivent encoder deux opérandes en plus de l'opcode, avec éventuellement où enregistrer le résultat sur les architectures 3-adresses. Par contre, les opérandes sont généralement des noms de registre, ce qui prend moins de place qu'une adresse mémoire. A la rigueur, les instructions avec une constante immédiate prennent un peu plus de place, car elles doivent encoder un nom de registre en plus de la constante et de l'opcode. Les instructions ''load-op'' des processeurs CISC sont un peu dans le même cas, sauf qu'il faut remplacer la constante par une adresse, qui a généralement la même taille. Les instructions sont donc en moyenne plus courte sur les processeurs à registre, du fait de la présence de registres. ==La micro-architecture des architectures à accumulateur sans registres d'indice== L'organisation interne d'une architecture à accumulateur est très différente de celle des processeurs à registre. Elle est plus ou moins la même pour tous ces processeurs, le point important étant que le chemin de données se résume à une ALU, un registre accumulateur et le bus de données. L'ALU est reliée au registre accumulateur, ainsi qu'au bus de données pour lire la seconde opérande, comme illustré ci-dessous. [[File:Accumulator.png|centre|vignette|upright=2|Accumulateur.]] ===L'unité de calcul et l'accumulateur=== Pour simplifier l'implémentation, le résultat est mémorisé dans un registre en sortie de l'unité de calcul. La raison est qu'on ne veut pas altérer l'accumulateur tant que le résultat final n'est pas totalement calculé. Rappelons que l'ALU ne fournit pas un résultat d'un seul bloc, mais que certains bits arrivent avant les autres, typiquement les bits de poids faible. Si le résultat était écrit dans l'accumulateur directement, il écraserait des bits de l’opérande en cours d'utilisation, ce qui fausserait le résultat. [[File:Accumulateur avec registre de sortie.png|centre|vignette|upright=2|Accumulateur avec registre de sortie]] Une autre solution utilisait un registre entre l'accumulateur et l'unité de calcul, appelé l’'''''accumulator latch'''''. Il remplace le registre en sortie de l'ALU, dans le sens où il permet d'écrire dans l'accumulateur sans effacer l'opérande, pendant que l’opération est en cours dans l'ALU. L'accumulateur est copié dans l’''accumulator latch'', avant que l'ALU démarre ses calculs. L'opérande est donc maintenue même si on commence à écrire le résultat dans l'accumulateur. Sur le 8085 d'Intel, l’''accumulator latch'' peut être initialisé avec une constante prédéfinie, ce qui permet d'implémenter certaines instructions très facilement. Par exemple, il peut être initialisé à 0, ce qui permet d'implémenter les instructions MOV et INC (incrémentation). Un MOV est équivalent à faire un OU entre le registre lu et 0. Une instruction INC initialise l'entrée de retenue de l'unité de calcul à 1, puis ajoute 0. La décrémentation est implémentée de la même manière, sauf que l’''accumulator latch'' est initialisé à -2 pour compenser l'entrée de retenue à 1. De plus, pour supporter les conversion de décimal à BCD, l’''accumulator latch'' peut être initialisé avec les constantes 0x00, 0x06, 0x60, or 0x66. L'''accumulator latch'' est relié à divers fils de commande provenant de l'unité de contrôle, qui décide quelle valeur d'initialisation utiliser en fonction de l'instruction décodée. [[File:Accumulator latch.png|centre|vignette|upright=2|Accumulator latch]] [[File:Chemin de données à un seul bus.png|vignette|Chemin de données à un seul bus]] De même, la seconde opérande, celle lue sur le bus de données, est mémorisée dans un registre en amont de l'ALU. C'était notamment très utile si le processeur utilisait une ALU plus courte que les opérandes, avec par exemple une ALU 4 bits pour un CPU 8 bits. Les anciens processeurs à accumulateur de type ''mainframe'' utilisaient des opérandes de grande taille, du genre 36 ou 48 bits, ce qui avait des conséquences sur l'unité de calcul utilisée, ainsi que sur la communication avec la mémoire. Il arrivait que de tels processeurs utilisaient des ALU sérielles, ou du moins octet-sérielles, plutôt qu'une véritable ALU 36 bits. Nous verrons aussi que c'est très utile sur les processeurs à accumulateur avec un bus interne unique, dans la suite du chapitre. ===La micro-architecture des architectures à accumulateur avec instructions LOAD/STORE=== Nous venons de voir comment est implémenté l'unité de calcul et l'accumulateur, et comment le tout est relié au bus de données. Mais cela ne permet que d'avoir un processeur à accumulateur rudimentaire, qui ne gére que des instructions arithmétiques et logiques. Il faut aussi gérer le cas des opérations LOAD/STORE et des modes d'adressages associés, mais c'est le séquenceur qui s'en occupe. Avec l'adressage absolu, les instructions LOAD et STORE ne font que connecter l'accumulateur au bus de données. L'instruction STORE nécessite de connecter l'accumulateur au bus de données, de manière à ce que le transfert des données se fasse de l'accumulateur vers le bus de données. L'instruction LOAD s'implémente de la même manière, sauf que le sens de transfert est inversé. Cependant, il existe une optimisation qui permet d'implémenter l'instruction LOAD sans ajouter de circuits. Pour cela, il suffit que l'unité de calcul gére les opérations ''pass through'', à savoir des opérations qui se contentent de recopier un opérande sur la sortie. Ici, l'idée est de recopier l'opérande provenant du bus de données. [[File:Architecture à accumulateur, microarchitecture.png|centre|vignette|upright=2|Architecture à accumulateur, microarchitecture]] Pour gérer nativement l'adressage indirect à registre, il suffit de connecter l'accumulateur au bus d'adresse. Le plus simple est d'ajouter un multiplexeur, comme illustré ci-dessous. La donnée lue est copiée dans l'accumulateur, ce qui fait qu'il vaut mieux utiliser un registre d’interfaçage, l'accumulateur n'étant pas utilisable à la fois pour le bus d'adresse et de données. [[File:Adressage indirect sur une architecture à accumulateur.png|centre|vignette|upright=2|Adressage indirect sur une architecture à accumulateur]] Pour finir, voyons comment le pointeur de pile et le ''program counter'' sont implémentés. Les architectures à accumulateur ont souvent des adresses de taille différente des données, ce qui fait qu'il vaut mieux utiliser un circuit incrémenteur dédié pour incrémenter le ''program counter'', plutôt que de l'incrémenter via l'unité de calcul. Les architectures à accumulateur ont parfois un pointeur de pile, qui gère une pile d'adresse de retour, pas une vraie pile d'appel. Aussi, le pointeur de pile est censé être incrémenté et décrémenté, pas plus. Pas d'addition ou de soustraction pour gérer des cadres de pile. Là encore, c'est la solution de l'incrémenteur séparé qui est retenue. Pour économiser des transistors, il n'y a qu'un seul incrémenteur partagé pour les deux. ==Les architectures à accumulateur à registres d'indice== Les architectures à accumulateur décrites dans la section précédente sont capables de faire de l'adressage indirect, mais n'incluent pas les modes d'adressage base + indice et absolus indicés, qui prennent une adresse et y ajoute un indice. Il s'agit des toutes premières architectures à accumulateur, comme les premiers ordinateurs IBM ou le fameux PDP-8. Mais par la suite, les processeurs à accumulateurs ont inclus un support des modes d'adressages indicés, grâce à des '''registres d'indice'''. ===Les registres d'indice=== Les registres d'indice stockent des indices de tableaux. Ils permettaient de supporter deux modes d'adressage : * Le '''mode d'adressage absolu indicé''' où une adresse fixe est additionnée à un indice variable. L'adresse fixe est intégrée dans l'instruction (adressage absolu), l'indice est dans un registre d'indice. * Le '''mode d'adressage base + indice''', où l'adresse est dans l'accumulateur et l'indice dans le registre d'indice. Les processeurs à accumulateur supportaient souvent des variantes des modes d'adressage précédents, où le registre d'indice était automatiquement incrémenté ou décrémenté à chaque utilisation. Au départ, ces processeurs n'utilisaient qu'un seul registre d'indice qui se comportait comme un second accumulateur spécialisé dans les calculs d'adresses mémoire. Le processeur supportait de nouvelles instructions qui utilisaient ce registre d'indice de façon implicite. Mais avec le temps, les processeurs finirent par incorporer plusieurs de ces registres. Les instructions de lecture ou d'écriture devaient alors préciser quel registre d'indice utiliser, en précisant un nom de registre d'indice, un numéro de registre d'indice. Il faut préciser que les registres d'indice ont une taille bien plus petite que l'accumulateur. Ils mémorisent des indices codés sur 16 bits ou moins, alors que l'accumulateur faisait typiquement 36 à 48 bits, parfois plus. La taille classique sur les anciens ''mainframes'' était de 15 bits, parfois moins. Il n'était pas rare de tomber sur des registres d'indice de 8 ou 9 bits. Leur petite taille rendait leur implémentation matérielle peu couteuse. Et c'est ce qui explique qu'ils étaient préférés à l'usage d'un second accumulateur. Un exemple est le cas du processeur Motorola 6809, un processeur à accumulateur qui contient deux registres d'indices nommés X et Y. L'accumulateur est noté D et fait 16 bits, il peut être parfois géré comme deux accumulateurs séparés A et B de 8 bits chacun. Il contenait aussi deux pointeurs de pile, l'un pour les programmes, l'autre pour le système d'exploitation, ainsi qu'un ''program counter''. Le registre de page était utilisé pour l'adressage absolu, comme vu dans le chapitre sur les modes d'adressage. [[File:6809 Internal Registers.svg|centre|vignette|upright=2|6809 Internal Registers]] Les processeurs IBM avaient autrefois plusieurs registres d'indice. Par exemple, l'IBM 704 avait trois registres d'indice de 15 bits. Leur contenu était soustrait de l'adresse absolue. Une instruction pouvait utiliser plusieurs registres d'indice pour adresser son opérande. Toute instruction utilisait trois bits pour sélectionner les registres d'indice utilisés. Fait assez original, lorsque plusieurs registres d'indice sont sélectionnés, le processeur n'additionne pas les trois registres à l'adresse de base. À la place, il fait un OU bit à bit entre les registres d'indice sélectionnés, et additionne le résultat à l'adresse. Les processeurs IBM à accumulateur ont fait ça sur toute la gamme IBM 700/7000, pour des raisons de compatibilité. Les processeurs suivants avaient 7 registres d'indices, mais conservaient les trois bits pour adresser les registres d'indices. Ils pouvaient fonctionner dans deux modes. Le premier mode est plus intuitif : les trois bits précisent un registre d'indice parmi les 7, qui est additionné avec l'adresse absolue. La valeur zéro indique qu'aucun registre d'indice n'est utilisé, ce qui explique qu'il y a 7 registres d'indice et non 8. Un second mode, compatible avec l'IBM 704, fait un OU logique entre les registres d'indice sélectionnés. Seuls les trois premiers registres d'indices peuvent être sélectionnés dans ce mode. Il y a deux instructions pour changer de mode : une pour passer en mode compatible, l'autre pour le quitter pour l'autre mode. Quelques rares processeurs à registres généraux ont utilisés des registres d'indice, en plus des registres généraux. Ce qui fait l'association que nous avons faite entre registres d'indice et architectures à accumulateur est imparfait, bien que solide sur le principe. Un exemple d'architecture de ce type sont les architectures de la série UNIVAC 1100/2200. Ils disposaient de 128 registres, qui étaient mappés en mémoire à partir de l'adresse mémoire 0, la majorité étant inaccessibles par le programmeur. Ils regroupaient 12 registres accumulateurs, 11 registres d'indice et 4 registres hybrides qui pouvaient servir soit de registre d'indice, soit de registres accumulateurs. ===La micro-architecture des architectures à accumulateur avec registres d'indice=== La présence de registres d'indice modifie grandement l'implémentation du processeur. En effet, il faut rajouter un banc de registre pour les registres d'indice. Le banc de registre est monoport, car on a besoin de lire ou d'écrire un indice à la fois. Et il faut aussi potentiellement rajouter de quoi faire les calculs d'adresse. Deux solutions sont possibles : une ALU dédiée aux calculs d'adresse, une seule ALU pour toutes les opérations. Dans le premier cas, il y a une ALU séparée associée aux registres d'indice. L'ALU et les registres d'indice sont placés en sortie du séquenceur, en-dehors du chemin de données, la sortie de l'ALU est directement connectée au bus d'adresse. L'unité de calcul d'adresse peut être utilisée pour incrémenter le ''program counter''. Un défaut de cette approche est qu'elle ne gère pas l'adresse indirect à registre. [[File:Chemin de données sans support des pointeurs, avec adressage absolu indicé.png|centre|vignette|upright=2|Chemin de données sans support des pointeurs, avec adressage absolu indicé]] Une autre option utilise un bus interne, qui interconnecte tout le chemin de données. Dans sa version la plus simple, il est relié à l'accumulateur, l'unité de calcul, le banc de registre d'indice et le bus de données. Avec cette solution, l'ALU est utilisé à la fois pour calculer les adresses et pour les instructions de calcul. En reliant le bus interne au bus d'adresse à travers un multiplexeur, on gère naturellement les adressages indirects. [[File:Architecture à accumulateur avec registres associés.png|centre|vignette|upright=2.5|Architecture à accumulateur avec registres associés]] Il faut noter que sur les architectures Von Neumann, le séquenceur est relié à ce bus interne. En effet, charger une instruction demande de passer par le bus mémoire, donc par l’intermédiaire de ce bus internet. Le ''program counter'' est envoyé sur ce bus pour lire l'instruction, puis l'instruction lue est recopiée dans le registre d'instruction. Le chargement d'une instruction est donc géré comme une lecture des plus classiques. De plus, cela permet d'incrémenter le ''program counter'' au niveau de l'unité de calcul entière. Le ''program counter'' est envoyé sur le bus interne, incrémenté par l'ALU, puis le résultat est recopié dans le ''program counter'' via le bus interne. [[File:Architecture à accumulateur avec bus unique de type Von Neumann.png|centre|vignette|upright=2|Architecture à accumulateur avec bus unique de type Von Neumann]] ==Les processeurs bi-accumulateurs : le Motorola 6800== Le Motorola 6800 était un processeur 8 bits qui gérait des adresses de 16 bits. Il contenait deux accumulateurs nommés A et B, de 8 bits chacun. Il disposait aussi d'un registre d'indice, d'un pointeur de pile et d'un ''program counter'', tous les trois de 16 bits. Le registre d'état regroupait 6 bits, qu'on ne détaillera pas ici. [[File:MC6800 Processor Diagram.png|centre|vignette|upright=2|Interface et registres du MC6800.]] La présence des deux accumulateurs impactait surtout les instructions dyadiques. Les instructions monadiques avaient juste à préciser où se trouvait l'opérande : soit en RAM, soit dans le premier accumulateur, soit dans le second. Pour les opérations dyadiques, la gestion était totalement différente. En théorie, le premier opérande est dans un accumulateur, soit A, soit B. La seconde opérande vient soit de l'autre accumulateur, soit de la mémoire RAM. Et selon les instructions, tout variait. Pour l'addition, il y avait trois instructions. Les deux premières additionnaient un opérande provenant de la mémoire avec un accumulateur. L'instruction ADDA sélectionnait le premier accumulateur, l'instruction ADDB sélectionnait le second accumulateur. Mais une troisième instruction, l’instruction ABA, permettait d'additionner le contenu des deux accumulateurs. Pour les autres opérations, il n'était pas possible d'utiliser les deux accumulateurs en même temps. La seule possibilité était de faire une opération entre un accumulateur et un opérande venant de la mémoire. Toutes les instructions étaient en double, avec une copie par accumulateur. Par exemple, l'instruction ANDA faisait un ET entre l’accumulateur A et l'opérande mémoire, l'instruction ANDB faisait pareil avec l'accumulateur B. ==Les architectures hybrides registres-accumulateur== Il a existé quelques architectures qui étaient des hybrides entre architectures à accumulateur et architecture à registre. Elles avaient un accumulateur unique, couplé à un banc de registres généraux. D'autres registres étaient souvent présents, comme un registre d'état, des registres pour la pile, etc. Elles sont apparues assez tôt, dès les années 50. Et c'était à une époque où les ordinateurs étaient intégralement construits avec des tubes à vides et autres mémoires spécialisées. Par exemple, le Bull Gamma 3 était un ordinateur de type ''mainframe'' qui disposait de deux accumulateurs, de 5 registres généraux, de deux registres pour la pile, d'un registre à décalage utilisé pour les opérations arithmétiques et BCD, et d'un registre d'état rudimentaire de deux bits (un bit de signe, un bit pour les résultats de comparaison). Mais ces architectures sont restées assez confidentielles pendant les années 50 à 80. Par la suite, elles ont eu un regain de popularité durant les années 80-90. Elles ont alors servit de transition entre architectures à accumulateur proprement dites, et architectures à registres. Par exemple, les processeurs Intel 8 bits disposaient de 7 registres de 8 bits nommés A, B, C, D, E, F, H, L, SP. Ils correspondent respectivement à : * l'accumulateur A ; * six registres nommés B, C, D, E, H et L ; * le registre d'état F ; * le pointeur de pile SP. Sur ces processeurs, les instructions dyadiques devaient mettre leur premier opérande dans l'accumulateur, la seconde provenait soit des registres, soit de la RAM, soit d'une constante immédiate. Les opérations monadiques pouvaient lire leur opérande depuis l'accumulateur, les autres registres ou la RAM, tout était permis. Les registres d'indices disparaissaient sur ces architectures, en théorie. Les registres généraux pouvaient être utilisés comme registre d'indice ou pour stocker des opérandes, ils étaient assez versatiles pour servir de registres d'indices. La présence de registres a de nombreux avantages, comparé aux architectures à accumulateur pures. Les instructions arithmétiques sont plus rapide, lire un registre étant plus rapide qu'un accès RAM. Les performances sont donc augmentées. De plus, les instructions arithmétiques utilisant ces registres sont plus courtes : pas besoin d'encoder une adresse mémoire, un nom de registre suffit. Vu que les processeurs de l’époque avaient des instructions de taille variable, cela améliorait la densité de code. ===La micro-architecture des processeurs hybrides registres-accumulateur=== La micro-architecture de ces processeurs est très similaire à celle d'un processeur avec des registres d'indices. La seule différence est que le banc de registre contient des registres généraux et non des registres d'indices. Le banc de registre est systématiquement monoport, car il n'avait aucune raison d'être multiport. Pour les instructions dyadiques, seules à lire deux opérandes, il ne servait que pour la seconde opérande, la première était lue depuis l'accumulateur. Les processeurs hybrides registre-accumulateur se classent en deux types principaux : ceux qui ont un seul bus interne, et ceux qui en ont deux. Le premier cas est identique à celui vu précédemment pour les architectures à accumulateur avec registres d'indice, à la seule différence est que le banc de registre contient des registres généraux et non des registres d'indices. Afin de gérer l'adressage indirect à registre, le bus interne est connecté aux deux registres d’interfaçage mémoire. [[File:Architecture à accumulateur avec bus unique de type Von Neumann.png|centre|vignette|upright=2|Architecture à accumulateur avec bus unique de type Von Neumann]] Notons que cela permet aussi d'implémenter facilement les branchements indirects, car le bus interne permet d'échanger des adresses entre ''program counter'' et banc de registre. De même, il est possible d'utiliser l'ALU pour gérer les branchements indirects., en plus de l'utiliser pour incrémenter le ''program counter''. [[File:Single bus organization.jpg|centre|vignette|upright=2|Single bus organization]] Une autre solution utilisait deux bus internes : un connecté au bus de données, un autre relié au bus d'adresse. Le bus de commande mémoire était lui commandé par le séquenceur, l'unité de contrôle. L'intermédiaire entre ces deux bus était le banc de registre, ainsi que les autres registres. Le banc de registre était connecté aux deux bus interne, avec un port de lecture relié au bus d'adresse, un port de lecture-écriture relié au bus de données. Le port de lecture était utilisé pour l'adressage indirect, l'autre l'était pour le reste. [[File:Proz1-e.png|centre|vignette|upright=2|Intérieur d'un processeur.]] L'organisation à deux bus simplifiait la gestion du ''program counter'' et le chargement des instructions. Le ''program counter'' était soit séparé du banc de registre, soit placé dans le banc de registre. Les deux solutions étaient utilisés, tout dépend du processeur. Il faut noter que si le ''program counter'' est intégré au banc de registres, alors la microarchitecture du processeur est bien plus simple. L'envoi du ''program counter'' sur le bus d'adresse utilise le port de lecture relié au bus d'adresse, qui sert aussi pour l'adressage indirect. L'économie de circuits, sans compter la simplicité d'implémentation que cela implique, explique sans doute que la technique a été utilisée sur quelques processeurs anciens. Le ''program counter'' était généralement incrémenté par l'ALU, ce qui explique qu'il soit connecté au bus interne pour les données. La connexion est aussi utile pour gérer les branchements indirects, qui copient un registre dans le ''program counter'', ainsi que les branchements relatifs (addition dans l'ALU), voire les branchements directs (l'adresse vers laquelle brancher est envoyée en entrée de l'ALU et propagée en sortie avec une opération ''pass through''). [[File:Connexion du program counter sur les bus avec PC dans le banc de registres.png|centre|vignette|upright=2|Connexion du program counter sur les bus avec PC dans le banc de registres]] ===Les registres d'interruption avec un processeur hybride-accumulateur : l'exemple du Z80=== Un exemple de processeur registre-accumulateur à deux bus interne est le Z80, un processeur fortement inspiré des CPU Intel 8 bits. Il a un jeu d'instruction similaire, mais a diverses améliorations par rapport au design original. Notamment, le processeur a des registres séparés pour les interruptions. Sur le Z80, les registres généraux sont dupliqués avec 6 registres pour les interruptions et 6 registres pour les programmes autres. Leur copie pour les interruptions sont nommées B', C', D', E', H', L'. Les 12 registres sont placés dans le banc de registre, ce qui implique que le fenêtrage de registres est utilisé pour gérer la séparation entre registre d'interruption et normaux. Il faut noter que l'accumulateur et le registre d'état sont aussi dupliqués, leurs copies étant nommées A' et F'. Mais leur duplication se fait autrement que par fenêtrage de registre. En plus des registres précédents, le Z80 incorporait deux registres d'indice nommés X et Y, ainsi qu'un pointeur de pile SP et un ''program counter'' PC. Ils n'étaient pas dupliqués pour les interruptions. Les registres d'indice étaient reliés à une unité de calcul d'adresse. L'unité de calcul d'adresse prenait deux opérandes : un indice, et une adresse mémoire provenant soit du banc de registre, soit de l'accumulateur. En clair, l'unité de calcul d'adresse faisait le lien entre les deux bus internet : le bus de données interne en entrée, le bus d'adresse en sortie. À l'intérieur du processeur, tous les registres étaient regroupés dans un banc de registre unique, sauf les accumulateurs, les registres d'état et le ''program counter''. Bien que ce ne soit pas indiqué sur le schéma ci-dessous, le ''program counter'' est séparé du banc de registre, alors que le pointeur de pile est dedans. Le compteur IR est lui associé au ''program counter'', il est sorti du banc de registre. Le Z80 utilise lui aussi un incrémenteur séparé de l'ALU, qui est utilisé pour mettre à jour le ''program counter'', le pointeur de pile et un compteur de rafraichissement mémoire nommé IR sur le Z80 (pour rappel, le rafraichissement mémoire demande de balayer la mémoire d'adresse en adresse et été fait par le CPU à l'époque). De plus, il est utilisé pour les instructions INC et DEC qui incrémentent une opérande de 16 bits obtenue en combinant deux registres de 8 bits. En clair, l'incrémenteur du ''program counter'' a été réutilisé de beaucoup de manières originales. [[File:Z80 arch.svg|centre|vignette|upright=3|Microarchitecture du Z80.]] Le Z80 incorporait des instructions pour échanger le contenu des deux ensembles de registres, accumulateur inclus. Elles permettaient d'utiliser les registres d'interruption pour les programmes et réciproquement. En clair, cela doublait le nombre de registres si les interruptions n'étaient pas utilisées. * L'instruction EXX échangeait les deux fenêtres de registres généraux : les registres B, C, D, E, H et L devenaient les registres B', C', D', E', H' et L' et inversement. * L'instruction EX échangeait l'accumulateur et le registre d'état : les registres A,F devenait A',F' et inversement. Le fenêtrage de registre était modifié par l'instruction EXX, qui échangeait les deux fenêtres de registres. Pour l'implémenter, une bascule était ajouté au processeur, appelons-la la bascule INT. Elle était relié au bus d'adresse du banc de registre, dont elle mémorisait le bit de poids fort. L'instruction EXX inversait la valeur de la bascule INT. Il est aussi possible d'échanger le contenu des registres D,E et H,L avec une instruction dédiée, ce qui est là encore fait par deux bascules : une pour les registres D,E et une autre pour les registres H,L. Pour l'accumulateur et le registre d'état, le choix entre registre normal et registre d'interruption se faisait là encore par une seconde bascule appelée la bascule A. La bascule est reliée à un multiplexeur et un démultiplexeur, qui permet de choisir quel accumulateur relier à l'unité de calcul. L'instruction EX inverse le contenu de la bascule A. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=L'implémentation matérielle des branchements | prevText=L'implémentation matérielle des branchements | next=Les architectures à pile et mémoire-mémoire | nextText=Les architectures à pile et mémoire-mémoire }} </noinclude> o4v3qjahayubb9tko1zz6rtqbje5ac3 Les cartes graphiques/Le rendu d'une scène 3D : l'API graphique 0 83408 763801 763043 2026-04-16T19:28:11Z Mewtow 31375 /* Les render target */ 763801 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures :un avec la couleur non-éclairée de chaque pixel, un autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et éventuellement d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un pixel shader pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les ''shaders'' sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les ''shaders'' à chaque exécution. Fait amusant, il faut savoir que le pilote peut parfois remplacer les ''shaders'' d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des ''shaders'' alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le ''shader'' originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le ''shader'' alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire. Enfin, certains ''shaders'' sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de ''shaders'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} 3lgwua2dlc8cigr8bcyiw0gjvrwempd 763802 763801 2026-04-16T19:31:09Z Mewtow 31375 /* Les render target */ 763802 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les ''shaders'' sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les ''shaders'' à chaque exécution. Fait amusant, il faut savoir que le pilote peut parfois remplacer les ''shaders'' d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des ''shaders'' alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le ''shader'' originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le ''shader'' alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire. Enfin, certains ''shaders'' sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de ''shaders'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} ki79mlwiv2rr15d8jx0adtg5hy05p1d 763803 763802 2026-04-16T19:31:44Z Mewtow 31375 /* Les render target */ 763803 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. ===Les fonctionnalités de ''Multiple Render Targets''=== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. Le MRT accélère fortement les techniques de rendu différé, qui enregistrent plusieurs images séparées, qui sont combinées par un pixel shader pour obtenir l'image finale. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] L'intérêt initial était d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les ''shaders'' sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les ''shaders'' à chaque exécution. Fait amusant, il faut savoir que le pilote peut parfois remplacer les ''shaders'' d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des ''shaders'' alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le ''shader'' originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le ''shader'' alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire. Enfin, certains ''shaders'' sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de ''shaders'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} o5t5z979tw9l28qw7pwqr5qnh2veyvo 763804 763803 2026-04-16T19:39:39Z Mewtow 31375 /* Les fonctionnalités de Multiple Render Targets */ 763804 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. ===Les fonctionnalités de ''Multiple Render Targets''=== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. L'implémentation classique est que le ''pixel shader'' peut écrire son résultat dans une ou plusieurs textures. Précisons qu'il s'agit bien du ''pixel shader'' qui écrit dans une texture. Suivant le GPU, l'écriture dans les ''render target'' peut ou non passer par les ROPs. Il arrive que l'écriture dans un ROP passe par les ROPs, mais pas les autres. D'autres GPU acceptent de faire passer tous les ''render target'' par les ROPs, et mêrme de configurer le mélange ''alpyha'' différemment suivant le ''render target''. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] Le MRT accélère fortement les techniques de '''rendu différé''', qui enregistrent plusieurs images séparées, qui sont combinées par un '''pixel shader''' pour obtenir l'image finale. L'intérêt du rendu différé est d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les ''shaders'' sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les ''shaders'' à chaque exécution. Fait amusant, il faut savoir que le pilote peut parfois remplacer les ''shaders'' d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des ''shaders'' alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le ''shader'' originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le ''shader'' alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire. Enfin, certains ''shaders'' sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de ''shaders'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} 8vh8fr1e1wakzhegmi9iex4dn2vqgek 763813 763804 2026-04-16T20:24:43Z Mewtow 31375 /* Les fonctionnalités de Multiple Render Targets */ Déplacement autre chapitre 763813 wikitext text/x-wiki De nos jours, le développement de jeux vidéo, ou tout simplement de tout rendu 3D, utilise des API 3D. Les API 3D les plus connues sont DirectX, OpenGL, et Vulkan. L'enjeu des API est de ne pas avoir à recoder un moteur de jeu différent pour chaque carte graphique ou ordinateur existant. Elles fournissent des fonctions qui effectuent des calculs bien spécifiques de rendu 3D, mais pas que. L'application de rendu 3D utilise des fonctionnalités de ces API 3D, qui elles-mêmes utilisent les autres intermédiaires, les autres maillons de la chaîne. Typiquement, ces API communiquent avec le pilote de la carte graphique et le système d'exploitation. ==La description des API 3D les plus communes== Dans ce chapitre, nous n'allons pas faire de cours du DirextX, ulkan ou toute API précise. Toutes le API graphiques fonctionnent globalement sur les mêmes principes, que nous allons expliquer dans les grandes lignes. Les explications seront conçues pour que les personnes sans bagage de la programmation graphique puissent comprendre, seuls desbases très mineures en programmation seront nécessaires dans le pire des cas. ===Les ''draw calls''=== Une API 3D fournit un certain nombre de fonctions qu'un programmeur peut exécuter à loisir. La principale est la fonction qui dessine quelque chose dans le ''framebuffer''. Elle est appelée ''draw()'' dans la terminologie DirectX, gldraw pour OpenGL, vkcmddraw pour Vulkan. Une exécution de cette fonction est appelée un '''''draw call'''''. Un ''draw call''envoie des informations à la carte graphique, afin qu'elle affiche ce qui est demandé. Instinctivement, on pourrait croire que la fonction ''draw'' calcule tout l'image à afficher d'un seul coup, mais ce n'est pas le cas. En réalité, le moteur graphique d'un jeu effectue le rendu objet par objet, avec un ''draw call'' par objet. Plus il y a d'objets, plus le processeur exécutera de ''draw calls''. Diverses optimisations permettent d'économiser des ''draw calls'', mais cela ne change pas le fait que dessiner l'image finale demande plusieurs ''draw calls'', entre une centaine et plusieurs centaines de milliers suivant la complexité de la scène à rendre. Le fait de rendre une image objet par objet permet de nombreuses optimisations. Par exemple, il peut utiliser une première passe pour dessiner les objets opaques, puis une seconde pour les objets transparents. Tous les moteurs 3D font ainsi, car gérer la transparence est toujours compliqué, surtout avec un tampon de profondeur. Un autre avantage est que le moteur de jeu peut faciliter le travail de l'élimination des surfaces cachées. Par exemple, le moteur de jeu peut trier les objets selon leur profondeur, afin de les rendre du plus proche au plus lointain. Pour les objets opaques, cela permet d'éliminer les surfaces cachées à la perfection : aucun triangle/pixel caché par un autre ne sera rendu. Pour la transparence, cela permet un rendu idéal. Mais trier les objets selon leur profondeur prend alors du temps CPU, qu'il faut comparer à ce qui est gagné sur le GPU. Avant les années 2010 environ, le processeur faisait une bonne partie de l'élimination des surfaces cachées, dans le sens où il déterminait quels objets étaient cachés par d'autres. Il n'émettait pas de ''draw calls'' pour les objets complétement cachés par un autre objet opaque. Par contre, il travaillait au niveau des objets, alors que le GPU travaillait au niveau des triangles. Les objets partiellement cachés étaient gérés par le GPU, avec une élimination des surface cachées triangle par triangle. De nos jours, l'élimination des surfaces cachées est réalisée sur le GPU, dans sa totalité. L'idée est d'utiliser un ''shader'' séparé, un ''compute shader'', qui s'exécute avant toute autre opération de rendu. La scène 3D et tous les modèles sont dans la mémoire vidéo, et non en mémoire RAM. Le ''compute shader'' lit l'ensemble de la géométrie et élimine les surface cachées. On parle de '''''GPU driven rendering''''' pour désigner cette élimination des surfaces cachées réalisée sur le GPU (il faudrait aussi rajouter le choix du ''Level Of Detail'', mais passons. ===Les ''render target''=== Plus haut, j'ai dit qu'un ''draw call'' dessine une image dans le ''framebuffer''. Et il s'agit là du cas le plus important, mais certaines techniques de rendu demandent de dessiner des images intermédiaires, qui sont utilisées pour calculer l'image finale. Les images intermédiaires doivent alors être enregistrées ailleurs, par exemple dans une texture. L'idée générale d'enregistrer des images intermédiaires dans une texture, qui sont alors lues par un ''pixel shader'' pour des calculs d'éclairage, des filtres de post-traitement, ou autre. Autoriser d'enregistrer l'image finale dans une texture s'appelle du '''''render-to-texture'''''. Les techniques d'éclairage basées sur des ''shadowmap'' sont dans ce cas. Elles demandent de rendre la scène 3D deux fois : une fois du point de vue de la source de lumière, puis une seconde fois pour obtenir l'image finale. L'idée est que les pixels invisibles depuis la source de lumière, mais visibles depuis la caméra, sont dans l'ombre. La scène rendue depuis la caméra doit donc être mémorisée quelque part, de préférence dans une texture appelée une ''shadowmap''. Une autre utilisation est l'application de filtres de post-traitement, comme du bloom, de la profondeur de champ, etc. L'idée est de mémoriser l'image initiale, sans post-traitement, dans une texture. Puis, un ''shader'' lit cette texture, applique un filtre dessus, et mémorise le résultat dans une autre texture ou dans le ''framebuffer'' s'il calcule l'image finale. Pour cela, les API 3D modernes permettent de préciser où enregistrer l'image finale : dans le ''framebuffer'', dans une texture, dans une simple portion de mémoire, etc. Les endroits où l'image finale peut être rendue s'appellent des '''''render target'''''. Les API modernes supportent de nombreux ''render target'', avec au minimum un ''framebuffer''. Initialement, les API anciennes ne supportaient que le ''framebuffer''. Puis le ''render-to-texture'' est apparu, puis d'autres formes de ''render target''. ===Les attributs de sommets et variables uniformes=== Lors d'un ''draw call'', certains paramètres vont rester constants, alors que d'autres vont varier d'un sommet à l'autre. Les paramètres qui varient d'un sommet à l'autre sont des '''attributs de sommet'''. Par exemple, prenons un sommet : sa position, sa couleur et ses coordonnées de texture sont des attributs du sommet. Les paramètres constants sont appelées des '''variables uniformes''', ou encore des ''uniforms''. Elles restent les mêmes pour un objet, mais varient d'un objet à l'autre. Un exemple est les matrices utilisées par les étapes de transformation et de projection. : Il y a la même chose avec les pixels, avec des attributs de pixels et des ''pixel uniforms'', la différence étant que les attributs de pixels sont calculés par la rastérisation. Les deux sont stockés différemment : les variables uniformes sont simplement intégrées dans les shaders, alors que les attributs sont placés dans le tampon de sommets. Il faut noter que les processeurs de shaders avaient autrefois des registres séparés pour les deux, et c'est toujours un peu le cas à l'heure actuelle. ===Les ''render states'' et les ''Pipeline State Object''=== Pour rendre un objet avec un ''draw call'', il faut préciser toutes informations nécessaires pour son rendu : la géométrie de l'objet représentée par une liste de triangles, les textures de l'objet, les ''shaders'' à exécuter (vertex ou pixel shaders), etc. Pour simplifier, nous allons regrouper ces informations en deux : un ''mesh'' qui représente la géométrie de l'objet, et le reste. La géométrie de l'objet est juste une liste de triangles. Le reste est regroupé dans un '''''render state''''', qui liste les textures, les shaders, quel ''render starget'' utiliser, et surtout : diverses options de configuration. Il n'y a qu'un seul ''render state'' actif, qui est mémorisé dans une portion de la RAM qui est toujours fixe. Pour les programmeurs, le ''render state'' est dans une variable globale, qui est lue directement par la fonction ''draw''. Si on veut rendre un objet, on doit mettre à jour le ''render state'' avant de lancer un ''draw call''. Un moteur graphique fait donc le travail suivant : * Pour chaque image : ** Mettre à jour la position de la caméra et autres ** Pour chaque objet, scène 3D inclue : *** 1 - Mettre à jour le ''render state'' *** 2 - Exécuter le ''draw call'' L'API 3D fournit des fonctions pour modifier le ''render state'', en plus de la fonction''draw''. A ce niveau, les anciennes API fonctionne différemment des API plus récentes comme DirectX 12, Vulkan et consort. Les anciennes API fournissaient plusieurs fonctions très spécialisées : certaines pour modifier les textures, d'autres pour changer les shaders, et un paquet d’autres pour modifier telle ou telle option de configuration. Par exemple, il y a probablement une fonction pour changer l'antialiasing. Les API modernes, comme DirectX 12 et Vulkan, permettent de mettre à jour le ''render state'' assez simplement. L'idée est de pré-calculer un ''render state'', qui est alors appelé un ''Pipeline State Object'' (PSO). Mettre à jour le ''render state'' demande alors juste de copier un PSO dans le render state, au lieu d'exécuter une dizaine ou centaine de fonctions pour obtenir le ''render state'' voulu. ===Les commandes graphiques=== L'API 3D traduit chaque ''draw call'' en une ou plusieurs '''commandes graphiques''', qui sont envoyées au ''driver'' du GPU. Les commandes en question sont assez diverses, mais elles sont spécifiques à chaque API graphique. Intuitivement, un ''draw call'' correspond à une commande graphique. Mais il peut y avoir d'autres types de commandes. Par exemple, copier une texture dans la mémoire vidéo demande d'exécuter une commande decopie, idem pour ce qui est de copier un objet/''mesh''. Pour comprendre en quoi un ''draw call'' peut se traduire en plusieurs commandes, prenons l'exemple suivant. On souhaite rendre un objet avec une texture bien précise, mais celle-ci n'a pas encore été chargée en mémoire vidéo. Dans ce cas, le ''draw call'' utilisera une commande pour copier la texture en mémoire vidéo, puis une seconde commande pour rendre l'objet dans le ''framebuffer''. Par contre, si la texture est déjà en mémoire vidéo, le ''draw call'' se traduira en une unique commande de rendu 3D. Il en est de même si le ''mesh'' n'est pas encore en mémoire vidéo : il faut exécuter une commande pour copier le ''mesh'' dans la mémoire vidéo. Il faut préciser que c'est la même chose si le ''draw call'' exécute un shader pour la première fois . Le ''driver'' doit compiler le shader pour la première fois, puis utiliser une commande pour mettre le résultat en mémoire vidéo, puis enfin effectuer le rendu. Cela explique le ''shader stuttering'' présent dans certains jeux récents, à savoir le petit ralentissement très énervant qui survient quand un ''shader'' est compilé en plein milieu d'une partie de jeu. Il est possible de limiter ce problème en compilant des shaders à l'avance, histoire de préparer le terrain pour les futurs ''draw calls'', dans une certaine mesure, mais cela demande du travail, qui n'est possible que le nombre de shaders à compiler reste faible. Les commandes graphiques sont envoyées au ''driver'' de la carte graphique. Il transforme alors ces commandes graphiques en '''commandes matérielles''', compréhensibles par le matériel, en quelque chose que le GPU peut exécuter. Le format des commandes matérielles est spécifique à chaque marquer de GPU, les GPU NVIDIA, Intel et AMD n'utilisent pas le même format de commande. Il est même possible que chaque GPU ait son propre format pour les commandes matérielles. Aussi, nous allons nous arrêter là pour le moment et laissons cela au chapitre sur le processeur de commande. ===Les optimisations liées aux ''draw calls''=== Il faut noter qu'un ''draw call'' demande d'utiliser un peu de puissance CPU : il faut traduire le ''draw call'' en commandes, les envoyer au ''driver'', qui fait du travail dessus, avant de les envoyer au GPU. Dans les premières versions d'OpenGL et DirectX, chaque ''draw call'' effectuait une commutation de contexte pour passer en espace noyau, afin de communiquer avec le ''driver''. Mais cette contrainte a depuis été relâchée, bien qu'elle marche dans les grandes lignes. Faire plein de ''draw calls'' aura donc un cout en CPU conséquent. Une première optimisation regroupe les objets avec le même ''render state'' ensemble. Sans cette optimisation, le moteur graphique met à jour le ''render state'' à chaque fois qu'il rend un objet. Avec cette optimisation, il met à jour le ''render state'' plus rarement. Par contre, le moteur graphique dépense du temps et de la puissance de calcul pour faire le tri. Il y a donc un compromis pas évident, qui ne vaudrait pas souvent le coup. Cependant, cette optimisation débloque d'autres optimisations très importantes, qui permettent de réduire le nombre de ''draw calls''. Plus haut, j'ai dit que le rendu se fait objet par objet, ''mesh'' par ''mesh''. Mais il s'agit là d'une simplification. En réalité, tout moteur graphique digne de ce nom incorpore des optimisations qui cassent cette règle. L'idée est d'éviter de faire plein de petits ''draw call'' : le GPU sera alors peu utilisé alors que le CPU fera beaucoup de travail. A l'inverse, faire peu de gros ''draw call'' entrainera une forte occupation du GPU au prix d'un cout CPU mineur. La première optimisation, appelée le '''''batching''''', regroupe plusieurs objets/''meshs'' en un seul ''draw call''. Par contre, cette optimisation ne marche que pour des objets ayant le même ''render state'', à l'exception de la géométrie. Les deux objets rendus ensemble doivent utiliser les mêmes shaders, les mêmes textures, etc. De plus, la fusion de deux objets doit se faire en mémoire RAM et est le fait du CPU, le GPU et la mémoire vidéo ne sont pas concernés. L'optimisation marche bien pour des objets statiques, ce qui permet de faire la fusion une fois pour toute, là où les objets dynamiques demandent de faire la fusion à chaque image. Diverses optimisations permettent de faciliter le ''batching''. L'idée est de rendre les différents ''render state'' plus similaires que la normale. Une optimisation de ce type est l'usage d''''atlas de textures'''. Un atlas de texture regroupe plusieurs textures en une seule texture. Deux objets avec les mêmes shaders et les mêmes options de configuration, peuvent ainsi partager le même ''render state'' quand ils adressent le même atlas de texture et non exactement les mêmes textures. Une seconde optimisation,appelée l''''''instancing''''', marche dans le cas où un objet dynamique est présent en plusieurs exemplaires à l'écran. L'idée est qu'au lieu d'utiliser un ''draw call'' par exemplaire, on utilise un seul ''draw call'' pour tous les exemplaires. L'avantage est que la carte n'a besoin de mémoriser qu'un seul exemplaire en mémoire vidéo, au lieu de mémoriser plusieurs copies du même ''mesh''. Il faut préciser que les différents exemplaires peuvent être placés à des endroits éloignés, être tournés différemment par rapport à la caméra, être dans des états d'animation différents, etc. Pour cela, le ''draw call'' précise, pour chaque exemplaire, comment l'orienter, le tourner et l'animer. Le ''render state'' contient pour cela une '''liste d'instances''' pour mémoriser ces informations pur chaque exemplaire. Le GPU peut consulter cette liste et la copier en mémoire vidéo. Une seule commande permet ainsi de rendre plusieurs exemplaires : le GPU lit la liste d'instance, le ''mesh'' et dessine automatiquement chaque exemplaire voulu de l'objet. Réduire le nombre de ''draw calls'' peut aussi se faire en évitant les objets peu détaillés, qui utilisent peu de polygones. Pour des objets trop peu détaillés, le GPU exécutera le ''draw call'' très vite et devra attendre que le CPU envoie le suivant. Le cout du ''draw call'' dominera le temps de calcul sur le GPU. Du temps de DirectX 9, l'idéal était d'avoir des objets d'au moins une centaine de triangles. De nos jours, les GPU les CPU sont plus puissant,ce qui fait que ce chiffre est à revoir, mais je n'en connais pas la valeur, même approximative. ==Le pipeline graphique== En plus de fournir des fonctions que les programmeurs peuvent utiliser, les API graphiques décrivent comment s'effectue le rendu d'une image. Elles spécifient comment doit être traité la géométrie, comment doit se faire la rastérisation, le filtrage de texture et bien d'autres choses. Pour le dire autrement, elles décrivent le pipeline graphique à utiliser. Pour rappel, le pipeline graphique comprend plusieurs étapes : plusieurs étapes de traitement de la géométrie, une phase de rastérisation, puis plusieurs étapes de traitement des pixels. Une API 3D comme DirectX ou OpenGl décrète quelles sont les étapes à faire, ce qu'elles font, et l'ordre dans lesquelles il faut les exécuter. Il n'existe pas un pipeline graphique unique et chaque API 3D fait à sa sauce, mais la plupart des API modernes ont des pipelines graphiques très similaires. Les seules différences majeures concernent la présence d'étapes facultatives, comme l'étape de tesselation, qui sont absentes des API anciennes. Pour donner un exemple, je vais prendre l'exemple d'OpenGL 1.0, une des premières version d'OpenGL, aujourd'hui totalement obsolète. Le pipeline d'OpenGL 1.0 est illustré ci-dessous. Il implémente le pipeline graphique de base, avec une phase de traitement de la géométrie (''per vertex operations'' et ''primitive assembly''), la rastérisation, et les traitements sur les pixels (''per fragment operations''). On y voit la présence du ''framebuffer'' et de la mémoire dédiée aux textures, les deux étant soit séparées, soit placée dans la même mémoire vidéo. La ''display list'' est une liste de commandes, de ''draw calls'', que la carte graphique doit traiter d'un seul bloc, chaque ''display list'' correspond au rendu d'une image, pour simplifier. Les étapes ''evaluator'' et ''pixel operations'' sont des étapes facultatives, qui ne sont pas dans le pipeline graphique de base, mais qui sont utiles pour implémenter certains effets graphiques. [[File:Pipeline OpenGL.svg|centre|vignette|upright=2|Pipeline d'OpenGL 1.0]] Le pipeline d'OpenGL 1.0 vu plus haut est très simple, comparé aux pipelines des API modernes. Pour comparaison, voici des schémas qui décrivent le pipeline de DirextX 10 et 11. Vous voyez que le nombre d'étapes n'est pas le même, que les étapes elles-mêmes sont légèrement différentes, etc. Toutes les API 3D modernes sont organisées plus ou moins de la même manière, ce qui fait que le pipeline des schémas ci-dessous colle assez bien avec les logiciels 3D anciens et modernes, ainsi qu'avec l'organisation des cartes graphiques (anciennes ou modernes). {| | style="vertical-align:top;" | [[File:D3D Pipeline.svg|vignette|D3D Pipeline]] |[[File:D3D11 Pipeline.svg|vignette|Pipeline de D3D 11]] |} ===L'implémentation peut être logicielle ou matérielle=== Une API graphique est avant tout quelque chose qui aide le programmeur. Il est d'ailleurs possible de les utiliser sans GPU, avec une simple carte d'affichage. Le rendu 3D se fait alors sur le processeur, et la carte d'affichage ne fait que recevoir l'image calculée et l'afficher. Et c'était le cas dans les années 90, avant l'invention des premières cartes accélératrices 3D. Le rôle des API 3D était de fournir des morceaux de code et un pipeline graphique, afin de simplifier le travail des développeurs, pas de déporter des calculs sur une carte accélératrice 3D. D'ailleurs, OpenGl et Direct X sont apparues avant que les premières cartes graphiques grand public soient inventées. Les premiers accélérateurs 3D sont arrivés sur le marché quelques mois après la toute première version de Direct X et Microsoft n'avait pas prévu le coup. OpenGL était lui encore plus ancien et ne servait pas initialement pour les jeux vidéos, mais pour la production d'images de synthèses et dans des applications industrielles (conception assistée par ordinateur, imagerie médicale, autres). OpenGL était l'API plébiscitée à l'époque, car elle était déjà bien implantée dans le domaine industriel, la compatibilité avec les différents OS de l'époque était très bonne, mais aussi car elle était assez simple à programmer. De nos jours, la grosse majorité du rendu 3D se fait sur le GPU. Les ''draw calls'' sont intégralement traités par le GPU, à quelques détails près. Mais les premières cartes accélératrices 3D ne le gérait que partiellement. Concrétement, les premières cartes de 3Dfx déléguaient le traitement de la géométrie au processeur, et ne s'occupaient que des étapes de rastérisation, de placage de texture et les étapes suivantes. Autant prévenir maintenant, nous verrons de nombreuses cartes graphiques de de genre dans le chapitre sur l'historique de l'accélération 3D. ===Les API imposent des contraintes sur le matériel=== Les API graphiques décrivent un pipeline, mais fournissent aussi d'autres contraintes. Par exemple, elles fournissent des régles sur la manière dont doit être faite la rastérisation. Elle disent plus ou moins quel doit être le résultat attendu par le programmeur. Et les GPU doivent respecter ces règles, ils doivent effectuer le rendu de manière à avoir un résultat identique à celui spécifié par l'API. Notez ma formulation quelque peu alambiquée, qui cache un point important : les GPU font comme si ! Je dis faire comme si, car il se peut que le matériel fasse autrement, mais pour un résultat identique. Tant que l'image finale est celle attendue par l'API 3D, le GPU a le droit de prendre des raccourcis, d'éliminer des calculs inutiles, d'utiliser un algorithme de rastérisation différent, etc. Par exemple, il arrive que la carte graphique fasse certaines opérations en avance, comparé au pipeline imposé par l'API, pour des raisons de performance. Typiquement, effectuer du ''culling'' ou les tests de profondeur plus tôt permet d'annuler de nombreux pixels invisibles à l'écran, et donc d'éliminer beaucoup de calculs inutiles. Mais la carte graphique doit cependant corriger le tout de manière à ce que pour le programmeur, tout se passe comme l'API 3D l'ordonne. De manière générale, sans même se limiter à l'ordonnancement des étapes du pipeline graphique, les règles imposées par les API 3D sont des contraintes fortes, qui contraignent les cartes graphiques dans ce qu'elles peuvent faire. De nombreuses optimisations sont rendues impossibles à cause des contraintes des API 3D. ==Le pilote de carte graphique== Le pilote de la carte graphique est un logiciel qui s'occupe de faire l'interface entre les API 3D et la carte graphique. En théorie, le système d'exploitation est censé jouer ce rôle, mais il n'est pas programmé pour être compatible avec tous les périphériques vendus sur le marché. Le pilote d'un périphérique sert justement à ajouter ce qui manque : ils ajoutent au système d'exploitation de quoi reconnaître le périphérique, de quoi l'utiliser au mieux. Avant toute chose, précisons que les systèmes d'exploitation usuels (Windows, Linux, MacOsX et autres) sont fournis avec un pilote de carte graphique générique, compatible avec la plupart des cartes graphiques existantes. Rien de magique derrière cela : toutes les cartes graphiques vendues depuis plusieurs décennies respectent des standards, comme le VGA, le VESA, et d'autres. Et le pilote de base fournit avec le système d'exploitation est justement compatible avec ces standards minimaux. Mais le pilote ne peut pas profiter des fonctionnalités qui sont au-delà de ce standard. L'accélération 3D est presque inexistante avec le pilote de base, qui ne sert qu'à faire du rendu 2D très basique, juste assez pour afficher l’interface de base du système d'exploitation. Par exemple, certaines résolutions ne sont pas disponibles et les performances sont loin d'être excellentes. Si vous avez déjà utilisé un PC sans pilote de carte graphique installé, vous avez certainement remarqué qu'il était particulièrement lent. Le pilote de la carte graphique gère beaucoup de choses. Comme tout pilote de périphérique, il gère la communication entre procersseur et GPU, via des techniques communes comme les interruptions, le ''pooling'' ou le ''DMA''. Plus évident, il s'occupe de la gestion de la mémoire vidéo, à savoir que c'est lui qui place les textures ou les modèles 3D dedans, il place le ''framebuffer'', les ''render target'' et tout ce qui réside en mémoire vidéo. Il s'occupe aussi des fonctionnalités liées à l'affichage : initialiser la carte graphique, fixer la résolution, le taux de rafraichissement, gérer le curseur de souris matériel, etc. Mais surtout, le pilote de périphérique s'occupe de l'exécution des ''draw call'' et des changements de ''render state''. Dans ce qui suit, nous allons nous intéresser aux fonctionnalités spécifiques au rendu 3D. ===Les commandes matérielles, compréhensibles par le GPU=== Pour rappel, les API 3Denvoient des '''commandes graphiques''' au pilote de périphérique. Les commandes graphiques sont standardisées, spécifiques à chaque API, et surtout : indépendantes du matériel. Le matériel ne comprend pas ces commandes graphiques ! A la place, le GPU comprend des '''commandes matérielles''', spécifiques à chaque marque de GPU, si ce n'est à chaque GPU. Lors du passage à une nouvelle génération de GPU, des commandes matérielles peuvent apparaître, d'autres disparaître, d'autre voient leur fonctionnement légèrement altéré, etc. Le pilote de la carte graphique doit convertir les commandes graphiques de l'API 3D, en commandes matérielles que le GPU peut comprendre. : La traduction des commandes se fait dans le pilote en espace utilisateur, alors que leur envoi au GPU est le fait du pilote en espace noyau. L'envoi des commandes à la carte graphique ne se fait pas directement. La carte graphique n'est pas toujours libre pour accepter une nouvelle commande, soit parce qu'elle est occupée par une commande précédente, soit parce qu'elle fait autre chose. Il faut alors faire patienter les données dans une '''file de commandes''', où les commandes matérielles attendent leur tour, dans l'ordre d'arrivée. Elle est placée soit dans une portion de la mémoire vidéo, soit est dans la mémoire RAM. Si la file de commandes est plein, le driver n'accepte plus de demandes en provenance des applications. Une file de commandes pleine est généralement mauvais signe : cela signifie que la carte graphique est trop lente pour traiter les demandes qui lui sont faites. Par contre, il arrive que la file de commandes soit vide : dans ce cas, c'est simplement que la carte graphique est trop rapide comparé au processeur, qui n'arrive alors pas à donner assez de commandes à la carte graphique pour l'occuper suffisamment. ===La compilation des ''shaders''=== Le pilote de carte graphique traduit les ''shaders'' en code machine que le GPU peut exécuter. En soi, cette étape est assez complexe, et ressemble beaucoup à la compilation d'un programme informatique normal. Les ''shaders'' sont écrits dans un langage de programmation de haut niveau, comme le HLSL ou le GLSL, avant d'être pré-compilés vers un langage dit intermédiaire. Le langage intermédiaire, comme son nom l'indique, sert d'intermédiaire entre le code source écrit en HLSL/GLSL et le code machine exécuté par la carte graphique. Il ressemble à un langage assembleur, mais reste malgré tout assez générique pour ne pas être un véritable code machine. Par exemple, il y a peu de limitations quant au nombre de processeurs ou de registres. En clair, il y a deux passes de compilation : une passe de traduction du code source en langage intermédiaire, puis une passe de compilation du code intermédiaire vers le code machine. Notons que la première passe est réalisée par le programmeur des ''shaders'', alors que la seconde est le fait du pilote du GPU. L'avantage est que la compilation prend moins de temps, comparé à compiler directement du code HLSL/GLSL. Le gros du travail à été fait lors de la première passe de compilation et le pilote graphique ne fait que finir le travail. Autant dire que cela économise plus le processeur que si on devait compiler complètement les ''shaders'' à chaque exécution. Fait amusant, il faut savoir que le pilote peut parfois remplacer les ''shaders'' d'un jeu vidéo à la volée. Les pilotes récents embarquent en effet des ''shaders'' alternatifs pour les jeux les plus vendus et/ou les plus populaires. Lorsque vous lancez un de ces jeux vidéo et que le ''shader'' originel s'exécute, le pilote le détecte automatiquement et le remplace par la version améliorée, fournie par le pilote. Évidemment, le ''shader'' alternatif du pilote est optimisé pour le matériel adéquat. Cela permet de gagner en performance, voire en qualité d'image, sans pour autant que les concepteurs du jeu n'aient quoique ce soit à faire. Enfin, certains ''shaders'' sont fournis par le pilote pour d'autres raisons. Les anciennes cartes graphiques avaient des circuits de T&L pour traiter la géométrie, mais elles ont disparues sur les machines récentes. Par souci de compatibilité, les circuits de T&L doivent être émulés sur les GPU récents. Sans cette émulation, les vieux jeux vidéo conçus pour exploiter le T&L et d'autres technologies du genre ne fonctionneraient plus du tout. Émuler les circuits fixes disparus sur les cartes récentes est justement le fait de ''shaders'' fournit par le pilote de carte graphique. {{NavChapitre | book=Les cartes graphiques | prev=La mémoire unifiée et la mémoire vidéo dédiée | prevText=La mémoire unifiée et la mémoire vidéo dédiée | next=Le processeur de commandes | nextText=Le processeur de commandes }}{{autocat}} 6msr3zmas10hwp5s2ob7i6kncsn3he2 Fonctionnement d'un ordinateur/Les coprocesseurs : FPU et IO 0 83778 763763 763644 2026-04-16T13:52:11Z Mewtow 31375 /* La pseudo-pile de registres */ 763763 wikitext text/x-wiki Les '''coprocesseurs''' sont des processeurs secondaires qui complémentent un processeur principal. Ils permettent de déléguer certains calculs à un processeur secondaire, afin de décharger le processeur principal. Un détail important est que le processeur principal et le coprocesseur sont très différents : ils n'ont pas le même jeu d'instruction, n'ont pas les mêmes performances, et bien d'autres différences. Nous en avions déjà vu dans le chapitre sur l'architecture de base, où nous avions analysé d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail ==Les différents types de coprocesseurs== Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties. Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80. Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici. [[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]] Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants. ===Les coprocesseurs faiblement et fortement couplés=== À ce stade du cours, nous allons distinguer les coprocesseurs faiblement couplés et fortement couplés. La distinction est que les premiers sont assez détachés du processeur, alors que les seconds ne le sont pas. La distinction n'est pas très claire, aussi j'ai décidé de prendre cette définition : les premiers sont traités comme des entrées-sorties, alors que les seconds sont intégrés sur le bus mémoire. Avec les '''coprocesseurs fortement couplés''', le CPU et le coprocesseur sont connectés sur le même bus mémoire. Le programme exécuté contient des instructions à destination du CPU, d'autres à destination du coprocesseur, dont l'encodage est différent. Les deux surveillent le bus mémoire et décident à qui est destinée l'instruction. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. Pour cela, le CPU envoie son ''program counter'' sur le bus d'adresse, ce qui entraine l'apparition d'une instruction sur le bus de données. Là, le processeur et le coprocesseur reçoivent l'instruction et la décodent. Si l'instruction est destinée au processeur, le CPU l'exécutera, alors que le coprocesseur la traitera comme un NOP. Et inversement si l'instruction est destinée au coprocesseur. Les '''coprocesseurs faiblement couplés''' sont des entrées-sorties mappées en mémoire, avec des registres d'interfaçage. Le processeur écrit une instruction et ses opérandes dans ces registres d'interfaçage, et le coprocesseur fait les calculs dans son coin. Le processeur récupère le résultat quelques cycles plus tard, en le lisant dans un autre registre d’interfaçage. La récupération du résultat peut se faire avec du ''pooling'' ou des ''interruptions inter-processeurs''. Le coprocesseur peut envoyer une interruption au processeur principal pour dire qu'il a terminé son travail. Parfois, les interruptions peuvent aller dans l'autre sens. Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80. Les coprocesseurs sonores sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore. Et ce n'est possible que si ces coprocesseurs sont faiblement couplés. Mais quelques coprocesseurs arithmétiques sont dans ce cas aussi, comme on le verra plus bas. [[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]] Il existe cependant des cas assez difficiles à classer. Nous verrons le cas des Motorola 68881 dans ce qui suit, qui sont un mélange des deux solutions. ===Les coprocesseurs arithmétiques : quelques généralités=== Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques. Et ce pour une raison très simple : nous avons déjà vu les coprocesseurs I/O et les coprocesseurs sonores dans un chapitre antérieur. Pour rappel, les coprocesseurs sonores sont des cartes sons, ou du moins une partie de carte son. De plus, on a plus de documentation sur les coprocesseurs arithmétiques. Il faut dire que c'étaient des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants Ils étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel. Le problème ne se pose pas sur les consoles de jeu, mais il est assez rare que les consoles de jeu incorporent des coprocesseurs arithmétiques. Il existe cependant des exceptions. Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre. ==Le jeu d'instruction x87 d'Intel== Un exemple de coprocesseur arithmétique est celui de l'extension x87 qui a ajouté les nombres flottants aux processeurs x86. Pour rappel, les processeurs x86 sont ceux qui sont présents à l'intérieur des PC, à savoir tous les processeurs Intel et AMD actuels. Il s'agit d'un jeu d'instruction qui a reçu de nombreux ajouts au cours du temps. Les ajouts en question sont appelés des '''extensions x86'''. Nous allons maintenant voir celle qui a ajouté la gestion des flottants au x86. L'extension x87 n'est plus utilisée depuis l'arrivée des CPU 64 bits, car elle a été remplacée par l'extension SSE. Sur les tout premiers processeurs x86, le support des nombres flottants n'était pas implémenté. A la place, ils utilisaient des coprocesseurs arithmétiques, appelés des '''coprocesseurs x87'''. Ils travaillaient en tandem avec un processeur x86 normal, et ne géraient que des instructions arithmétiques sur des flottants. Ils étaient capables d'exécuter les 4 opérations de base (add, sub, mul, div), la racine carrée, les opérations trigonométriques sinus, cosinus et tangente, l'arc tangente, et des instructions de calcul de logarithmes ou d'exponentielles. ===La pseudo-pile de registres=== Les coprocesseurs arithmétiques x87 avaient 8 registres flottants, ainsi que 3 registres de contrôle. Les 8 registres x87 sont ordonnés et numérotés de 0 à 7 Les registres x87 sont organisés sous la forme d'une pile de registres, similaire à une pile d'assiette, que l'on remplit dans un ordre bien précis. Lors du chargement d'un opérande de la RAM vers les registres, il n'est pas possible de décider du registre de destination. Si on veut ajouter des flottants dans nos registres, on doit les « remplir » dans un ordre de remplissage imposé : on remplit d'abord le registre 7, puis le 6, puis le 5, et ainsi de suite jusqu'au registre 0. Si on veut ajouter un flottant dans cette pile de registres, celui-ci sera stocké dans le premier registre vide dans l'ordre de remplissage indiqué au-dessus. Prenons un exemple, les 3 premiers registres sont occupés par un flottant et on veut charger un flottant supplémentaire : le 4ᵉ registre sera utilisé pour stocker ce flottant. La même chose existe pour le « déremplissage » des registres. Imaginez que vous souhaitez déplacer le contenu d'un registre dans la mémoire RAM et effacer complètement son contenu. On ne peut pas choisir n'importe quel registre pour faire cela : on est obligé de prendre le registre non vide ayant le numéro le plus grand. {| |[[File:Pseudo-pile x87. - PUSH.png|vignette|upright=2|Pseudo-pile x87 - chargement d'une opérande.]] |[[File:Pseudo-pile x87 - POP.png|vignette|upright=2|Pseudo-pile x87 - retrait d'une opérande.]] |} Les instructions à un opérande dépilent le flottant au sommet de la pile. Les instructions dyadiques peuvent dépiler les deux opérandes au sommet de la pile, mais elles peuvent aussi utiliser d'autres modes d'adressage. Elles peuvent aller chercher la seconde opérande en RAM, en fournissant une adresse. Mais elles peuvent aussi adresser n'importe quel autre registre de la pile en fournissant son numéro de registre. Avec ce dernier mode d'adressage, le processeur agit comme une sorte de processeur à accumulateur, avec le sommet de la pile servant d'accumulateur. En somme, les registres de la pile sont adressables, du moins pour ce qui est de gérer la seconde opérande. C'est cette particularité qui vaut le nom de pseudo-pile à cette organisation à mi-chemin entre une pile et une architecture à registres. ===Les instructions x87=== Maintenant, voyons quelles instructions les FPU x87 devaient gérer. L'extension x87 est en effet un jeu d'instruction, qui décrit quelles instructions doivent être gérées. Les instructions utilisent des opcodes inutilisés dans le jeu d'instruction x86, qui sont détournés pour fonctionner sur le x87. On trouve évidemment des instructions de calculs, bien évidemment compatibles avec la norme IEE754 : l'addition FADD, la soustraction FSUB, la multiplication FMUL et la division FDIV. Mais il y a aussi la racine carrée FSQRT. La FPU x87 implémente aussi des instructions trigonométriques, qui ne sont pas supportées par la norme IEEE 754. Elle gérait l'instruction FCOS pour le cosinus, l'instruction FSIN pour le sinus, l'instruction FPTAN pour la tangente, l'instruction FPATAN pour l'arc tangente, ainsi que des instructions de calcul de logarithmes ou d'exponentielles. La FPU x87 dispose aussi d'instructions de comparaisons compatibles avec la norme IEEE 754, capables de comparer le flottant au sommet de la pile avec un autre nombre qui peut être flottant ou entier ! Voici une liste de quelques instructions de comparaisons supportées par les FPU 87 : * FCOM : compare le contenu du registre 0 avec une constante flottante ; * FCOMI : compare le contenu des registres 0 et 1 ; * FICOM : compare le contenu du registre 0 avec une constante entière ; * FTST : compare le registre numéroté 0 avec la valeur 0. Le x87 avait aussi des instructions de calcul de la valeur absolue (FABS) ou encore de changement de signe (FCHS). Les instructions de calcul n'ayant besoin que d'un seul flottant pour s'exécuter, comme les opérations trigonométriques ou la valeur absolue, utilisent le flottant situé au sommet de la pile. Les instructions dyadiques (multiplication, addition, soustraction et autres) vont agir différemment suivant la situation. Elles peuvent prendre les deux flottants les plus haut placés dans cette pile, prendre le flottant au sommet de la pile, utiliser une donnée en provenance de la mémoire, ou encore utiliser le flottant le plus haut placé et un flottant stocké dans l'importe quel registre de cette pile de registres. La pile de registre était donc une sorte de mélange entre un accumulateur et 7 registres adressables. Pour charger des opérandes dans la pile d'opérande, l'extension x87 fournit trois instructions d'accès mémoire. {|class="wikitable" |- ! Instruction ! Description |- ! FLD | Charge un nombre flottant depuis la mémoire vers notre pile de registres vue au-dessus. Cette instruction peut charger un flottant codé sur 32 bits, 64 bits ou 80 bits |- ! FSTP | Déplace le contenu d'un registre vers la mémoire. Une autre instruction existe qui est capable de copier le contenu d'un registre vers la mémoire sans effacer le contenu du registre : c'est l'instruction FST |- ! FXCH | Échange le contenu du dernier registre non vide dans l'ordre de remplissage (celui situé au sommet de la pile) avec un autre registre |} D'autres instructions existent qui chargent certaines constantes (PI, 1, 0, certains logarithmes en base 2) dans le registre au sommet de la pile de registres. ===Les registres de contrôle et d'état=== Pour gérer la pseudo-pile, les registres pour les flottants sont associés à un registre d'état nommé '''''Tag Word'''''. Le registre ''Tag Word'' indique, pour chaque registre flottant, s'il est vide ou non. Avouez que c'est pratique pour gérer la pile de registres vue au-dessus ! Le registre ''tag word'' fait 16 bits, ce qui fait 2 bits pour chacun des 8 registres. Ces deux bits contiennent des informations sur le contenu du registre de données réservé. * Si ces deux bits valent 00, le registre contient un flottant « normal » différent de zéro ; * Si ces deux bits valent 01, le registre contient une valeur nulle : 0 ; * Si ces deux bits valent 10, le registre contient un NAN, un infini, ou un dénormal ; * Si ces deux bits valent 11, le registre est vide et ne contient pas de nombre flottant. Le processeur x87 contient aussi deux registres d'état, nommés ''Control Word'' et ''Status Word''. Le registre ''Status Word'' contient quelques bits, certains utilisés pour gérer la pseudo-pile, d'autres non. Il fait lui aussi 16 bits et c'est un registre d'état qui est utilisé pour qu'un programme puisse comprendre la cause d'une exception. Il contient le numéro du registre juste au-dessus du sommet de la pile, le numéro du premier registre vide dans l'ordre de remplissage. Mais il contient surtout des bits mis à 1 en cas de débordement de flottant, de division par zéro, lorsqu'un calcul a pour résultat un dénormal, etc. {|class="wikitable" |- ! Bit !! Utilité |- ! TOP | Trois bits, qui codent le numéro du premier registre vide dans la pile de registre |- ! U | Détecte les ''underflows'' : est mis à 1 en cas d'''underflows''. |- ! O | | Détecte les ''overflows'' : est mis à 1 en cas d'''overflows''. |- ! Z | Prévient qu'une division par zéro a eu lieu. Est mis à 1 si c'est le cas. |- ! D | Bit est mis à 1 lorsqu'un résultat de calcul est un dénormal ou lorsqu'une instruction doit être exécutée sur un dénormal |- ! I | Bit mis à 1 lors de certaines erreurs telles que l'exécution d'une instruction de racine carrée sur un négatif ou une division du type 0/0 |} Le registre ''Control Word'' fait 16 bits et configure la gestion des arrondis. {|class="wikitable" |- ! Bit !! Utilité |- ! Infinity Control | S'il vaut zéro, les infinis sont tous traités comme s'ils valaient +∞. S'il vaut un, les infinis sont traités normalement |- ! Rouding Control | C'est un ensemble de deux bits qui détermine le mode d'arrondi utilisé * 00 : vers le nombre flottant le plus proche : c'est la valeur par défaut ; * 01 : vers - l'infini ; * 10 : vers + l'infini ; * 11 : vers zéro |- ! Precision Control |`Ensemble de deux bits qui détermine la taille de la mantisse de l'arrondi du résultat d'un calcul. En effet, on peut demander à notre FPU d'arrondir le résultat de chaque calcul qu'elle effectue. Cette instruction ne touche pas à l'exposant, mais seulement à la mantisse. La valeur par défaut de ces deux bits est 11 : notre FPU utilise donc des flottants double précision étendue. Les valeurs 00 et 10 demandent au processeur d'utiliser des flottants non pris en compte par la norme IEEE 754. * 00 : mantisse codée sur 24 bits ; * 01 : valeur inutilisée ; * 10 : mantisse codée sur 53 bits ; * 11 : mantisse codée sur 64 bits |} Les instructions x87 sont codées sur au minimum deux octets. Le premier octet commence toujours la suite de bit 11011, qui indique que c'est une instruction destinée au coprocesseur. Le 11011 était appelé le code d'échappement, sa mnémonique en assembleur était ESC. Le tout est suivi par 6 bits d'opcode, et 5 bits pour le mode d'adressage. Le tout était regroupé comme suit : 1101 1xxx aaxx xaaa, avec x les bits de l'opcode et a pour le mode d'adressage. Le tout était suivi par des opérandes, selon le mode d'adressage. ===Les doubles arrondis avec les registres x87=== Les nombres flottants sont standardisés par l'IEEE, avec le standard IEEE754. La norme impose deux formats de flottants : les flottants 32 bits et les flottants 64 bits. Cependant, la FPU x87 utilisait des flottants codés sur 80 bits, non-standardisés par l'IEEE754, ce qui posait quelques problèmes. Les instruction FLD et FSTP sont l'équivalent des instructions LOAD et STORE, mais pour les flottants x87. Elles peuvent lire/écrire soit un flottant 32 bits, soit un flottant 64 bits, soit un flottant 80 bits. En pratique, les compilateurs préféraient utiliser des flottants 32 et 64 bits, les flottants 80 bits n'étaient pas utilisés. Les raisons sont assez diverses : une meilleure performance, des histoires de compatibilité et de standard IEEE754, etc. Le problème est qu'utiliser des flottants 32/64 bits impose de faire des arrondis lors des accès mémoire. Lors d'une lecture d'un flottant 32/64 bit, celui-ci est étendu sur 80 bits. Et inversement pour l'écriture : les flottants 80 bits sont arrondis pour rentrer dans 32/64 bits. Et faire des calculs intermédiaires sur 80 bits avant de les arrondir ne donne pas le même résultat que si on avait fait les calculs sur 32 ou 64 bits nativement. Les résultats intermédiaires ont une précision supérieure, donc le résultat peut être différent. [[File:Phénoméne de double arrondi sur les coprocesseurs x87.png|centre|vignette|upright=3.0|Phénomène de double arrondi sur les coprocesseurs x87]] Pour citer un exemple, sachez que des failles de sécurité de PHP et de Java aujourd'hui corrigées étaient causées par ces arrondis supplémentaires. Pour limiter la casse, il existe une solution : sauvegarder tout résultat d'un calcul sur un flottant directement dans la mémoire RAM. Comme cela, on se retrouve avec des calculs effectués uniquement sur des flottants 32/64 bits ce qui supprime pas mal d'erreurs de calcul. Mais le cout en performance est tellement drastique que cette solution n'est que rarement utilisée. ==Les coprocesseurs x87 et leurs précurseurs== Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres. ===Les précurseurs : l'Intel 8231 et le 8232=== L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas. L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division). : La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée. [[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]] Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit. Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile. Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié. {|class="wikitable" |- ! A0, RD, WR !! Action |- | 000 || Lecture de l'octet depuis les registres de données. |- | 010 || Écriture de l'octet dans les registres de données. |- | 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement. |- | 101 || Lecture du registre d'état. |} Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232. * READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut. * END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0. * EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0. Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non. Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle ! Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz. ===L'intel 8087 et ses successeurs=== [[File:Intel 8087.svg|vignette|Intel 8087]] Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée. L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL. Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel. Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage. Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande. Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation. Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend. Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants. Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant. [[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]] Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser). De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe. L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins. [[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]] L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul. Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs. Pour finir, voici quelques liens sur la microarchitecture du 8087 : * [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip]. * [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter]. * [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered]. * [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode]. * [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die]. * [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip]. ===Les coprocesseurs x87 de Weitek=== Intel n'a pas été le seul fabricant à commercialiser des coprocesseurs x87. Weitek et de nombreuses autres entreprises s'y sont mises. Weitek a commercialisé plusieurs coprocesseurs : un premier coprocesseur appelé le 1067, le 1167, le 2167, le 3167 et le 4167. Ils sont tous rétrocompatibles entre eux, à savoir que le 4167 ne fait qu'ajouter des fonctionnalités au 3167, qui lui-même ajoute des fonctionnalités au 2167, et ainsi de suite. Ils gèrent tous les quatre opérations de base, ainsi que le calcul de la racine carrée. Le '''Weitek 1067''' avait pour particularité d'être fourni en pièces détachées, avec trois circuits séparés : un circuit de contrôle, une ALU flottante, et un multiplieur/diviseur. Ces trois pièces détachées étaient censées être soudées sur la carte mère. Le '''Weitek 1167''' regroupait ces trois pièces détachées sur une carte d'extension ISA. Par la suite, le '''Weitek 2167''' regroupa les trois pièces détachées dans un seul circuit imprimé. Il n'y avait pas de registres flottants adressables, ni de pile de registres. À la place, l'ALU et le multiplieur/diviseur intégraient deux registres pour les opérandes et un registre accumulateur. Les opérandes étaient présentés sur le bus de données et l'ALU les mémorisait dans deux registres internes de 64 bits chacun, nommés A et B. Le coprocesseur lisait les opérandes depuis ces deux registres et mémorisait le résultat dans un registre de résultat interne. Il envoyait alors le résultat sur le bus de données, prévenait le CPU avec une interruption, et le CPU récupérait le résultat sur le bus. Il faut noter que toutes les communications avec l'ALU passent par le bus de données, appelé le bus X. La transmission d'un flottant 32 bits se faisait en un cycle d'horloge, vu que le bus était de 32 bits. Par contre, la transmission d'un flottant 64 bits se faisait en deux cycles. À part le bus de données, il y avait un bus de commande, relié à l'unité de contrôle 1163. Elle envoyait l'opcode sur une entrée dédiée, notée F. Les bits de commande L, CSL, CSUS, CUSX, U commandent la lecture des opérandes ou leur écriture. L'unité de contrôle recevait le registre d'état via une sortie dédiée nommée S ou STATUS. [[File:Weitek WTL1167 arch.svg|centre|vignette|upright=2.5|Weitek WTL1167.]] Pour charger les opérandes dans l'ALU, celle-ci intégrait diverses entrées de commande nommées L0, L1, L2, L3 et CSL. * Le signal CSL est le ''Chip Select Load'', ce qui indique qu'il est mis à 1 lors d'une lecture. * Le bit L0 à 1 indique qu'un opcode est envoyé sur l'entrée F, l'entrée pour l'opcode est recopiée dans un registre interne, qui n'est pas un registre d'instruction vu que l'ALU n'a pas de décodeur. * Les bits L1 et L2 indiquent si ce qu'il y a sur le bus est : un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. * Le registre L3 indique quel est le registre de destination : A ou B. Les signaux U, CSUS et CSUX servaient pour l'envoi du résultat sur le bus. Ils précisaient s'il fallait copier un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. Le '''Weitek 3167''' était conçu pour fonctionner en tandem avec un CPU Intel 386, alors que le '''Weitek 4167''' était prévu pour aller avec un Intel 486, mais ils fonctionnaient de la même manière au-delà de quelques détails. Contrairement à leurs prédécesseurs, ils intégraient 32 registres flottants de 32 bits. Un registre pair et un registre impair pouvaient être concaténés pour mémoriser un opérande 64 bits. Les opérations se faisaient soit entre deux registres flottants, soit entre un registre flottant et l'opérande présentée sur le bus de données. Le résultat est stocké dans un registre flottant. Leur registre de status contenait les résultats des comparaisons flottantes, ainsi que 8 bits pour les exceptions flottantes. Le registre de contrôle avait un champ pour configurer les arrondis, mais aussi un masque d'exceptions de 8 bits disant quels bits d'exceptions ignorer dans le registre de statut. Le coprocesseur est mappé en mémoire, ce qui fait qu'il a des adresses réservées dans lesquelles le processeur peut lire/écrire, pour lui envoyer une instruction, envoyer un opérande ou récupérer un résultat. Les adresses en question sont les adresses COOO OOOOh à COOO FFFFh, ce qui fait que le coprocesseur est adressé si les 16 bits de poids fort de l'adresse valent COOO. Par contre, les 65536 adresses réservées n'étaient pas associées à de la mémoire, pas même aux registres. A la place, les 16 bits de poids faible de l'adresse encodaient une instruction au coprocesseur, à savoir un opcode de 6 bits deux numéros de registres. Le processeur communique avec le coprocesseur en envoyant l'instruction sur le bus d'adresse, et éventuellement un opérande sur le bus de données. L'opérande vient généralement des registres, par simplicité, car cela permet de tout envoyer en une seule écriture. Par exemple, une écriture du registre EAX à l'adresse COOO OOOOh va copier le registre EAX sur le bus de données, et envoyer l'opcode de l'addition (0000) sur le bus d'adresse avec deux numéros de registre. Le coprocesseur fait alors une addition entre le registre flottant sélectionné, et l'opérande sur le bus de données copiée depuis EAX. ===Le Motorola 68881 et le 68882=== Le 68881 de Motorola était conçu pour fonctionner avec les CPU 68020 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Pour une instruction coprocesseur, il lisait les opérandes en RAM, puis envoyait instruction et opérandes au 68881. Il continuait son travail dans son coin, et récupérait le résultat quelques cycles plus tard. Malgré le fait qu'il y ait des instructions destinées au coprocesseur, le 68881 n'était pas un coprocesseur fortement couplé. A la place, le 68881 était géré comme une entrée-sortie, du point de vue du CPU. Il était mappé en mémoire, avait une entrée ''Chip Select'' commandé par décodage d'adresse. Il contenait aussi des registres d'interface, appelés des ''coprocessor interface registers'' (CIRs). Il y avait des registres pour l'opcode de l'instruction à exécuter, un autre pour chaque opérande, etc. Pour envoyer une instruction au 68881, le CPU avait juste à écrire dans les registres adéquats, idem pour charger les opérandes de l'instruction si besoin. Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Fait étonnant, cela ressemble beaucoup à ce qui est fait avec les coprocesseurs x87 : usage de flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, le jeu d'instruction est complétement différent. Les coprocesseurs Motorola avaient aussi un registre de statut et un registre de contrôle, guère plus. Le registre de statut mémorisait les conditions classiques, mais aussi des bits pour les exceptions qui ont été levées lors d'un calcul. Le registre de contrôle mémorisait de quoi configurer les arrondis, mais aussi un masque pour indiquer quelles exceptions ignorer dans le registre de statut. Pareil que pour les processeurs précédents, donc. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les sections critiques et le modèle mémoire | prevText=Les sections critiques et le modèle mémoire | next=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> 15365jcd5owhkyi3o6vdmjvjekxk916 763770 763763 2026-04-16T14:11:21Z Mewtow 31375 Déplacement d'une section dans un chapitre précédent (un ancien chapitre orphelin, réutilisé pour l'occasion) 763770 wikitext text/x-wiki Les '''coprocesseurs''' sont des processeurs secondaires qui complémentent un processeur principal. Ils permettent de déléguer certains calculs à un processeur secondaire, afin de décharger le processeur principal. Un détail important est que le processeur principal et le coprocesseur sont très différents : ils n'ont pas le même jeu d'instruction, n'ont pas les mêmes performances, et bien d'autres différences. Nous en avions déjà vu dans le chapitre sur l'architecture de base, où nous avions analysé d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail ==Les différents types de coprocesseurs== Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties. Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80. Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici. [[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]] Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants. ===Les coprocesseurs faiblement et fortement couplés=== À ce stade du cours, nous allons distinguer les coprocesseurs faiblement couplés et fortement couplés. La distinction est que les premiers sont assez détachés du processeur, alors que les seconds ne le sont pas. La distinction n'est pas très claire, aussi j'ai décidé de prendre cette définition : les premiers sont traités comme des entrées-sorties, alors que les seconds sont intégrés sur le bus mémoire. Avec les '''coprocesseurs fortement couplés''', le CPU et le coprocesseur sont connectés sur le même bus mémoire. Le programme exécuté contient des instructions à destination du CPU, d'autres à destination du coprocesseur, dont l'encodage est différent. Les deux surveillent le bus mémoire et décident à qui est destinée l'instruction. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. Pour cela, le CPU envoie son ''program counter'' sur le bus d'adresse, ce qui entraine l'apparition d'une instruction sur le bus de données. Là, le processeur et le coprocesseur reçoivent l'instruction et la décodent. Si l'instruction est destinée au processeur, le CPU l'exécutera, alors que le coprocesseur la traitera comme un NOP. Et inversement si l'instruction est destinée au coprocesseur. Les '''coprocesseurs faiblement couplés''' sont des entrées-sorties mappées en mémoire, avec des registres d'interfaçage. Le processeur écrit une instruction et ses opérandes dans ces registres d'interfaçage, et le coprocesseur fait les calculs dans son coin. Le processeur récupère le résultat quelques cycles plus tard, en le lisant dans un autre registre d’interfaçage. La récupération du résultat peut se faire avec du ''pooling'' ou des ''interruptions inter-processeurs''. Le coprocesseur peut envoyer une interruption au processeur principal pour dire qu'il a terminé son travail. Parfois, les interruptions peuvent aller dans l'autre sens. Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80. Les coprocesseurs sonores sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore. Et ce n'est possible que si ces coprocesseurs sont faiblement couplés. Mais quelques coprocesseurs arithmétiques sont dans ce cas aussi, comme on le verra plus bas. [[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]] Il existe cependant des cas assez difficiles à classer. Nous verrons le cas des Motorola 68881 dans ce qui suit, qui sont un mélange des deux solutions. ===Les coprocesseurs arithmétiques : quelques généralités=== Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques. Et ce pour une raison très simple : nous avons déjà vu les coprocesseurs I/O et les coprocesseurs sonores dans un chapitre antérieur. Pour rappel, les coprocesseurs sonores sont des cartes sons, ou du moins une partie de carte son. De plus, on a plus de documentation sur les coprocesseurs arithmétiques. Il faut dire que c'étaient des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants Ils étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel. Le problème ne se pose pas sur les consoles de jeu, mais il est assez rare que les consoles de jeu incorporent des coprocesseurs arithmétiques. Il existe cependant des exceptions. Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre. ==Les coprocesseurs pour les CPU x86== Dans le chapitre précédent, nous avons parlé du jeu d'instruction x87, qui ajoutait le support des nombres flottants aux PC x86. Formellement, le x87 est apparu sur le coprocesseur 8087, un coprocesseur Intel prévu pour être utilisé avec un 8086. Mais ce n'a pas été le premier coprocesseur flottant pour PC. Il a existé quelques coprocesseurs avant lui, qui ne respectaient pas le standard x87. Et ne parlons pas des coprocesseurs Motorola et autres, qui suivaient un autre standard et ne fonctionnaient pas avec un CPU x86. Dans cette section, nous allons voir les coprocesseurs conçus pour les PC, qui fonctionnaient en tandem avec un CPU x86. Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres. ===Les précurseurs : l'Intel 8231 et le 8232=== L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas. L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division). : La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée. [[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]] Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit. Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile. Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié. {|class="wikitable" |- ! A0, RD, WR !! Action |- | 000 || Lecture de l'octet depuis les registres de données. |- | 010 || Écriture de l'octet dans les registres de données. |- | 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement. |- | 101 || Lecture du registre d'état. |} Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232. * READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut. * END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0. * EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0. Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non. Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle ! Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz. ===L'intel 8087 et ses successeurs=== [[File:Intel 8087.svg|vignette|Intel 8087]] Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée. L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL. Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel. Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage. Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande. Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation. Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend. Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants. Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant. [[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]] Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser). De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe. L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins. [[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]] L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul. Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs. Pour finir, voici quelques liens sur la microarchitecture du 8087 : * [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip]. * [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter]. * [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered]. * [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode]. * [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die]. * [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip]. ===Les coprocesseurs x87 de Weitek=== Intel n'a pas été le seul fabricant à commercialiser des coprocesseurs x87. Weitek et de nombreuses autres entreprises s'y sont mises. Weitek a commercialisé plusieurs coprocesseurs : un premier coprocesseur appelé le 1067, le 1167, le 2167, le 3167 et le 4167. Ils sont tous rétrocompatibles entre eux, à savoir que le 4167 ne fait qu'ajouter des fonctionnalités au 3167, qui lui-même ajoute des fonctionnalités au 2167, et ainsi de suite. Ils gèrent tous les quatre opérations de base, ainsi que le calcul de la racine carrée. Le '''Weitek 1067''' avait pour particularité d'être fourni en pièces détachées, avec trois circuits séparés : un circuit de contrôle, une ALU flottante, et un multiplieur/diviseur. Ces trois pièces détachées étaient censées être soudées sur la carte mère. Le '''Weitek 1167''' regroupait ces trois pièces détachées sur une carte d'extension ISA. Par la suite, le '''Weitek 2167''' regroupa les trois pièces détachées dans un seul circuit imprimé. Il n'y avait pas de registres flottants adressables, ni de pile de registres. À la place, l'ALU et le multiplieur/diviseur intégraient deux registres pour les opérandes et un registre accumulateur. Les opérandes étaient présentés sur le bus de données et l'ALU les mémorisait dans deux registres internes de 64 bits chacun, nommés A et B. Le coprocesseur lisait les opérandes depuis ces deux registres et mémorisait le résultat dans un registre de résultat interne. Il envoyait alors le résultat sur le bus de données, prévenait le CPU avec une interruption, et le CPU récupérait le résultat sur le bus. Il faut noter que toutes les communications avec l'ALU passent par le bus de données, appelé le bus X. La transmission d'un flottant 32 bits se faisait en un cycle d'horloge, vu que le bus était de 32 bits. Par contre, la transmission d'un flottant 64 bits se faisait en deux cycles. À part le bus de données, il y avait un bus de commande, relié à l'unité de contrôle 1163. Elle envoyait l'opcode sur une entrée dédiée, notée F. Les bits de commande L, CSL, CSUS, CUSX, U commandent la lecture des opérandes ou leur écriture. L'unité de contrôle recevait le registre d'état via une sortie dédiée nommée S ou STATUS. [[File:Weitek WTL1167 arch.svg|centre|vignette|upright=2.5|Weitek WTL1167.]] Pour charger les opérandes dans l'ALU, celle-ci intégrait diverses entrées de commande nommées L0, L1, L2, L3 et CSL. * Le signal CSL est le ''Chip Select Load'', ce qui indique qu'il est mis à 1 lors d'une lecture. * Le bit L0 à 1 indique qu'un opcode est envoyé sur l'entrée F, l'entrée pour l'opcode est recopiée dans un registre interne, qui n'est pas un registre d'instruction vu que l'ALU n'a pas de décodeur. * Les bits L1 et L2 indiquent si ce qu'il y a sur le bus est : un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. * Le registre L3 indique quel est le registre de destination : A ou B. Les signaux U, CSUS et CSUX servaient pour l'envoi du résultat sur le bus. Ils précisaient s'il fallait copier un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. Le '''Weitek 3167''' était conçu pour fonctionner en tandem avec un CPU Intel 386, alors que le '''Weitek 4167''' était prévu pour aller avec un Intel 486, mais ils fonctionnaient de la même manière au-delà de quelques détails. Contrairement à leurs prédécesseurs, ils intégraient 32 registres flottants de 32 bits. Un registre pair et un registre impair pouvaient être concaténés pour mémoriser un opérande 64 bits. Les opérations se faisaient soit entre deux registres flottants, soit entre un registre flottant et l'opérande présentée sur le bus de données. Le résultat est stocké dans un registre flottant. Leur registre de status contenait les résultats des comparaisons flottantes, ainsi que 8 bits pour les exceptions flottantes. Le registre de contrôle avait un champ pour configurer les arrondis, mais aussi un masque d'exceptions de 8 bits disant quels bits d'exceptions ignorer dans le registre de statut. Le coprocesseur est mappé en mémoire, ce qui fait qu'il a des adresses réservées dans lesquelles le processeur peut lire/écrire, pour lui envoyer une instruction, envoyer un opérande ou récupérer un résultat. Les adresses en question sont les adresses COOO OOOOh à COOO FFFFh, ce qui fait que le coprocesseur est adressé si les 16 bits de poids fort de l'adresse valent COOO. Par contre, les 65536 adresses réservées n'étaient pas associées à de la mémoire, pas même aux registres. A la place, les 16 bits de poids faible de l'adresse encodaient une instruction au coprocesseur, à savoir un opcode de 6 bits deux numéros de registres. Le processeur communique avec le coprocesseur en envoyant l'instruction sur le bus d'adresse, et éventuellement un opérande sur le bus de données. L'opérande vient généralement des registres, par simplicité, car cela permet de tout envoyer en une seule écriture. Par exemple, une écriture du registre EAX à l'adresse COOO OOOOh va copier le registre EAX sur le bus de données, et envoyer l'opcode de l'addition (0000) sur le bus d'adresse avec deux numéros de registre. Le coprocesseur fait alors une addition entre le registre flottant sélectionné, et l'opérande sur le bus de données copiée depuis EAX. ==Le Motorola 68881 et le 68882== Le 68881 de Motorola était conçu pour fonctionner avec les CPU 68020 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Pour une instruction coprocesseur, il lisait les opérandes en RAM, puis envoyait instruction et opérandes au 68881. Il continuait son travail dans son coin, et récupérait le résultat quelques cycles plus tard. Malgré le fait qu'il y ait des instructions destinées au coprocesseur, le 68881 n'était pas un coprocesseur fortement couplé. A la place, le 68881 était géré comme une entrée-sortie, du point de vue du CPU. Il était mappé en mémoire, avait une entrée ''Chip Select'' commandé par décodage d'adresse. Il contenait aussi des registres d'interface, appelés des ''coprocessor interface registers'' (CIRs). Il y avait des registres pour l'opcode de l'instruction à exécuter, un autre pour chaque opérande, etc. Pour envoyer une instruction au 68881, le CPU avait juste à écrire dans les registres adéquats, idem pour charger les opérandes de l'instruction si besoin. Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Fait étonnant, cela ressemble beaucoup à ce qui est fait avec les coprocesseurs x87 : usage de flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, le jeu d'instruction est complétement différent. Les coprocesseurs Motorola avaient aussi un registre de statut et un registre de contrôle, guère plus. Le registre de statut mémorisait les conditions classiques, mais aussi des bits pour les exceptions qui ont été levées lors d'un calcul. Le registre de contrôle mémorisait de quoi configurer les arrondis, mais aussi un masque pour indiquer quelles exceptions ignorer dans le registre de statut. Pareil que pour les processeurs précédents, donc. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Les sections critiques et le modèle mémoire | prevText=Les sections critiques et le modèle mémoire | next=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> d91vwu66nt7fk71yq2s1eywi2231y7o 763772 763770 2026-04-16T14:12:00Z Mewtow 31375 /* Le Motorola 68881 et le 68882 */ 763772 wikitext text/x-wiki Les '''coprocesseurs''' sont des processeurs secondaires qui complémentent un processeur principal. Ils permettent de déléguer certains calculs à un processeur secondaire, afin de décharger le processeur principal. Un détail important est que le processeur principal et le coprocesseur sont très différents : ils n'ont pas le même jeu d'instruction, n'ont pas les mêmes performances, et bien d'autres différences. Nous en avions déjà vu dans le chapitre sur l'architecture de base, où nous avions analysé d'anciennes consoles de jeu. Mais il est temps de voir ces coprocesseurs en détail ==Les différents types de coprocesseurs== Les coprocesseurs peuvent se classer en plusieurs catégories : les coprocesseurs sonores, arithmétiques, et d'entrées-sorties. Les '''coprocesseurs sonores''' sont une sorte d'ancêtre des cartes son, utilisés sur les anciennes consoles de jeux vidéo, comme La Nintendo 64, la Playstation et autres consoles antérieures. Ils s'occupaient respectivement de calculer tout ce qui a trait au son. Pour donner un exemple, on peut citer la console Neo-géo, qui disposait de deux processeurs travaillant en parallèle : un processeur principal, et un coprocesseur sonore. Le processeur principal était un Motorola 68000, alors que le coprocesseur sonore était un processeur Z80. Les '''coprocesseur d'IO''' sont dédiés à l'accès aux entrées-sorties. Pour simplifier, ce sont des contrôleurs DMA programmables, capables d'effectuer quelques opérations de branchements et de calcul. Nous en avions parlé dans le chapitre sur les entrée-sorties, je ne reviendrais pas dessus ici. [[File:Asmp 2.gif|centre|vignette|upright=2|Co-processeur pour l'accès aux entrées-sorties.]] Les '''coprocesseurs arithmétiques''' sont dédiés aux calculs en virgule flottante. Ils étaient utilisés à une époque où les CPU ne géraient que des calculs entiers (en binaire ou en BCD). Un exemple est le coprocesseur flottant x87, complémentaire des premiers processeurs Intel x86. Il y a eu la même chose sur les processeurs Motorola 68000, avec deux coprocesseurs flottants appelés les Motorola 68881 et les Motorola 68882. Ils sont aujourd'hui tombés en désuétude, depuis que les CPU sont devenus capables de faire des calculs sur des nombres flottants. ===Les coprocesseurs faiblement et fortement couplés=== À ce stade du cours, nous allons distinguer les coprocesseurs faiblement couplés et fortement couplés. La distinction est que les premiers sont assez détachés du processeur, alors que les seconds ne le sont pas. La distinction n'est pas très claire, aussi j'ai décidé de prendre cette définition : les premiers sont traités comme des entrées-sorties, alors que les seconds sont intégrés sur le bus mémoire. Avec les '''coprocesseurs fortement couplés''', le CPU et le coprocesseur sont connectés sur le même bus mémoire. Le programme exécuté contient des instructions à destination du CPU, d'autres à destination du coprocesseur, dont l'encodage est différent. Les deux surveillent le bus mémoire et décident à qui est destinée l'instruction. Les instructions sont exécutées soit par le CPU, soit par le coprocesseur. En clair, le CPU et le coprocesseur se passent à la main à tour de rôle, ils ne travaillent pas en parallèle. Pour cela, le CPU envoie son ''program counter'' sur le bus d'adresse, ce qui entraine l'apparition d'une instruction sur le bus de données. Là, le processeur et le coprocesseur reçoivent l'instruction et la décodent. Si l'instruction est destinée au processeur, le CPU l'exécutera, alors que le coprocesseur la traitera comme un NOP. Et inversement si l'instruction est destinée au coprocesseur. Les '''coprocesseurs faiblement couplés''' sont des entrées-sorties mappées en mémoire, avec des registres d'interfaçage. Le processeur écrit une instruction et ses opérandes dans ces registres d'interfaçage, et le coprocesseur fait les calculs dans son coin. Le processeur récupère le résultat quelques cycles plus tard, en le lisant dans un autre registre d’interfaçage. La récupération du résultat peut se faire avec du ''pooling'' ou des ''interruptions inter-processeurs''. Le coprocesseur peut envoyer une interruption au processeur principal pour dire qu'il a terminé son travail. Parfois, les interruptions peuvent aller dans l'autre sens. Un exemple est celui des consoles néo-géo et Megadrive. Elles intègrent deux processeurs : un Motorola 68000 qui sert de processeur principal, un Z80 qui sert de processeur dédié à l'audio. Le MC68000 envoie des commandes au Z80, mais la communication ne va pas dans l'autre sens. Les deux processeurs communiquent via l'intermédiaire d'un ''IO arbiter chip'', qui gère les interruptions inter-processeur. Il contient un registre de 8 bits, dans lequel le MC68000 peut écrire un numéro d'interruption, qui indiquent quelle routine d'interruption exécuter. Lorsque le MC68000 écrit une valeur dedans, cela déclenche l’exécution automatique d'une interruption sur le Z80. Les coprocesseurs sonores sont tous dans ce cas. Le coprocesseur sonore exécute un programme pour gérer le son, qui est séparé au programme principal. Le programme principal communique avec le coprocesseur, mais c'est assez rare. Dans un jeu vidéo, cela arrive seulement quand il faut changer de musique ou déclencher un effet sonore. Et ce n'est possible que si ces coprocesseurs sont faiblement couplés. Mais quelques coprocesseurs arithmétiques sont dans ce cas aussi, comme on le verra plus bas. [[File:Architecture de la Megadrive et de la Néogeo.png|centre|vignette|upright=2.5|Architecture de la Megadrive et de la Néogeo]] Il existe cependant des cas assez difficiles à classer. Nous verrons le cas des Motorola 68881 dans ce qui suit, qui sont un mélange des deux solutions. ===Les coprocesseurs arithmétiques : quelques généralités=== Dans le reste de ce chapitre, nous allons surtout voir les coprocesseurs arithmétiques. Et ce pour une raison très simple : nous avons déjà vu les coprocesseurs I/O et les coprocesseurs sonores dans un chapitre antérieur. Pour rappel, les coprocesseurs sonores sont des cartes sons, ou du moins une partie de carte son. De plus, on a plus de documentation sur les coprocesseurs arithmétiques. Il faut dire que c'étaient des processeurs commerciaux, vendus autrefois en magasin, avec de la documentation destinée aux utilisateurs. Les coprocesseurs arithmétiques étaient spécialisés dans les calculs flottants Ils étaient optionnels et il était parfaitement possible de monter un PC qui n'en avait pas. En conséquence, les programmeurs devaient coder des programmes qui peuvent fonctionner avec et sans coprocesseur. La solution la plus simple était de fournir deux versions du logiciel : une sans usage du coprocesseur, et une autre qui en fait usage, plus rapide. Une autre solution était d'émuler les calculs flottants en logiciel. Le problème ne se pose pas sur les consoles de jeu, mais il est assez rare que les consoles de jeu incorporent des coprocesseurs arithmétiques. Il existe cependant des exceptions. Un exemple récent est de la console de jeu Nintendo DS. La console utilisait deux processeurs, un ARM9 et un ARM7, deux processeurs RISC qui ne pouvaient pas faire de division entière. Il s'agit pourtant d'opérations importantes dans le cas du rendu 3D, ce qui fait que les concepteurs de la console ont rajouté un coprocesseur spécialisé dans les divisions entières et les racines carrées. Le coprocesseur était adressable directement par le processeur, comme peuvent l'être la RAM ou les périphériques, et était traité comme une entrée-sortie comme une autre. ==Les coprocesseurs pour les CPU x86== Dans le chapitre précédent, nous avons parlé du jeu d'instruction x87, qui ajoutait le support des nombres flottants aux PC x86. Formellement, le x87 est apparu sur le coprocesseur 8087, un coprocesseur Intel prévu pour être utilisé avec un 8086. Mais ce n'a pas été le premier coprocesseur flottant pour PC. Il a existé quelques coprocesseurs avant lui, qui ne respectaient pas le standard x87. Et ne parlons pas des coprocesseurs Motorola et autres, qui suivaient un autre standard et ne fonctionnaient pas avec un CPU x86. Dans cette section, nous allons voir les coprocesseurs conçus pour les PC, qui fonctionnaient en tandem avec un CPU x86. Les premiers coprocesseurs de ce type étaient l'Intel 8231/8232, destinés à être utilisés avec le 8088, un processeur 8 bits. Par la suite, Intel a récidivé avec le 8087, qui était destiné pour servir en tandem avec le 8086. Il a été suivi par les Intel 187, le 287, le 387, le 487 et le 587, qui étaient censés servir avec les CPU 186, 286, 386, 486, etc. Mais d'autres compagnies ont crée des coprocesseurs x87, comme Weitek, Cyrix, AMD, Texas Instrument, et bien d'autres. ===Les précurseurs : l'Intel 8231 et le 8232=== L'Am9511 et Am9512 sont une des toutes premières FPU pour PC, si ce n'est les premières si on en croit AMD. Ils ont été licenciés par Intel sous le nom d'Intel 8231 et le 8232. Ils étaient conçus pour complémenter le CPU Intel 8080, mais on pouvait parfaitement les utiliser avec d'autres CPU, comme le Z80. La [http://ep.homeserver.hu/PDF/AM9511A-9512.pdf documentation AMD donnait même des exemples assez variés]. La raison est qu'on y accédait comme n'importe quelle entrée-sortie connectée au bus système. Ils étaient accessibles via ''pooling'', interruptions ou même via DMA. Nous expliquerons comment c'est possible plus bas. L'Intel 8231 ne supportait pas le jeu d'extension x87, qui est apparu après. Il gérait des nombres flottants de 32 bits, mais aussi des nombres en virgule fixe de 16 et 32 bits. Les flottants 32 bits suivaient globalement la norme IEEE 754, mais les nombres en virgule fixe utilisaient un format propriétaire. Il gérait les quatre opérations de base, mais aussi des calculs trigonométriques. Il utilisait pour cela du microcode, avec une approximation basée sur des polynômes de Tchebychev. L'intel 8232 supportait lui des flottants 32 et 64 bits, mais ne supportait que les quatre opérations de base (addition, soustraction, multiplication et division). : La documentation décrit ces flottants 32 bits comme étant de la double précision, mais c'est parce que la terminologie de l'époque n'était pas encore bien stabilisée. [[File:C8231A FPU PIN CONFIGURATION.png|thumb|Intel 8231 FPU PIN CONFIGURATION.]] Les broches de l'intel 8231 sont illustrées ci-contre. La plupart des broches nous sont familières : 8 broches pour le bus de données (qui fait 8 bits), une entrée d'horloge, une entrée de RESET, une entrée ''chip select'' pour le décodage d'adresse. Les broches restantes sont très intéressantes, mais on les verra dans ce qui suit. Toujours est-il que le coprocesseur est relié à un bus de 8 bits, alors que ses registres font 32 à 64 bits. Pour cela, le 8231/8232 lisait les opérandes octet par octet depuis le bus de données. Idem mais pour les écritures. Par contre, les instructions sont prévues pour faire 8 bits, pas plus. Pour avoir des instructions aussi courtes, la seule solution est d'utiliser une machine à pile et c'est ce que le 8231/8232 a fait. Précisons cependant que ce n'est pas la même pile de registre que la pile x87, mais c'était une sorte de pile similaire, qui a évolué pour donner la pile x87. Il s'agissait pour le coup d'une vraie pile, les opérations utilisaient systématiquement le sommet de la pile et l'opérande en dessous. Il n'y avait pas de possibilité d'adresser un opérande dans la pile. Le processeur intégrait 8 registres de 16 bits, organisés comme une pile. Les registres pouvaient être utilisés : soit comme une pile de 8 opérandes 16 bits, soit une pile de 4 opérandes 32 bits, soit une pile de 2 opérandes 64 bits. Les opérandes étaient empilées octet par octet dans le processeur. Ils étaient dépilés là aussi octet par octet. Pour cela, le 8231/8232 dispose de trois entrées nommées A0, RD et WR. Les trois bits décident s'il faut faire une lecture, une écriture, exécuter une instruction, ou lire le registre d'état. Les quatre opérations sont appelées des commandes dans la documentation Intel et AMD. Les trois entrées font donc office de bus de commande simplifié. {|class="wikitable" |- ! A0, RD, WR !! Action |- | 000 || Lecture de l'octet depuis les registres de données. |- | 010 || Écriture de l'octet dans les registres de données. |- | 111 || L'octet est l'opcode d'une instruction, qui est exécutée immédiatement. |- | 101 || Lecture du registre d'état. |} Le processeur principal envoyait des commandes à l'Intel 8231/8232, qui les exécutait dans son coin. Le 8231/8232 envoyait un signal END OF EXECUTION pour prévenir qu'il avait fini son travail, que la commande précédente était terminée. Il avait une broche dédiée, appelée END, dédiée à ça. Le coprocesseur avait donc une interface de communication asynchrone, qui se voit quand on étudie ses broches. Les broches suivantes servent à la communication asynchrone avec le 8231/8232. * READY est à 1 quand le 8231/8232 est libre, capable d'accepter une nouvelle instruction/commande. Il passe à 0 quand une instruction démarre, avec la commande 111 vue plus haut. * END indique que la commande précédente a terminé son exécution. Lorsque END passe à 1, BUSY passe automatiquement à 0. * EACK est une entrée sur laquelle le processeur dit qu'il a bien reçu le signal END, et que ce dernier peut être remis à 0. Ce système pouvait être utilisé avec du ''pooling'', avec des interruptions, voire du DMA. Avec des interruptions, la sortie END était utilisée comme sortie d'interruption, reliée au CPU ou au contrôleur d'interruption. Pour le ''pooling'', le registre d'état du 8231/8232 contenait un bit BUSY, qui indiquait si le coprocesseur était utilisé ou non. Un tel fonctionnement peut sembler étrange, et vous aurez l'impression que communiquer avec le coprocesseur est très lent. Mais cela prend tout son sens quand on connait le temps mis pour exécuter une instruction sur le coprocesseur. Une opération simple sur des flottants 32 bits prenait facilement une cinquantaine de cycles d'horloge, et c'était parmi les meilleurs temps de calcul. Il n'était pas rare d'avoir des opérations prenant plusieurs centaines, voire milliers de cycles d'horloge. Pas loin de 5000 cycles d'horloge pour une division de deux flottants 64 bits sur le 8232, plusieurs dizaines de milliers de cycles pour certaines opérations trigonométriques. Et le pire, c'était que c'était plus rapide que l'émulation logicielle ! Pas étonnant donc que le 8231/8232 aient été traités comme des entrées-sorties, à une époque ou tout était connecté sur un bus système assez rapide. Un autre avantage est que le 8231/8232 pouvaient fonctionner à une fréquence sans rapport avec celle du processeur. Par exemple, on pouvait utiliser un processeur à 1 MHz alors que le 8231/8232 allait à 4 MHz. Le coprocesseur faisait juste des calculs rapidement, comparé au CPU. Et ça a été utilisé sur certains systèmes Apple II. Ou encore, on pouvait utiliser un processeur légèrement plus rapide que le coprocesseur, avec quelques MHz de différence, comme un CPU à 5 MHz avec un coprocesseur de 2 MHz. ===L'intel 8087 et ses successeurs=== [[File:Intel 8087.svg|vignette|Intel 8087]] Le 8087 été fabriqué avec 65 000 transistors. Le 8087 avait pour particularité qu'il était connecté directement sur le bus mémoire, au même titre que le 8086. Mais le 8087 n'avait pas de bus d'adresse et de données séparé. Le processeur utilisait un bus multiplexé. Il avait 20 broches pour se connecter au bus : 16 d'entre elles servaient alternativement de bus d'adresse et de données. L'interface avec le bus était donc un peu compliquée. L'intel 387 était le coprocesseur associé au 386 d'Intel. Il était le premier coprocesseur à s'intégrer sur un bus de 32 bits. Il a été décliné en plusieurs versions, dont certaines sont spécifiques à un modèle de 386. Par exemple, le i386SX était une version simplifiée du 386 initial, qui avait notamment un bus de seulement 16 bits. Et de ce fait, il avait son propre coprocesseur i387SX, qui était adapté à un bus de 16 bits. De même, le i386SL était adapté aux ordinateurs portables et avait son propre coprocesseur i387SL. Tout ce qui va suivre est valide pour tous les coprocesseurs x87 de marque Intel. Le processeur central lisait des instructions, en envoyant le ''program counter'' sur le bus d'adresse, les instructions étaient récupérées sur le bus de données. Là, les deux processeurs déterminaient si l'instruction chargée était destinée au coprocesseur ou au CPU. Pour cela, les instructions x87 commencent toutes par la suite de bit 11011, qui permet de savoir facilement si une instruction est destinée au coprocesseur. Le 11011 était suivi par un opcode et un mode d'adressage. Si le mode d'adressage demandait de lire un opérande mémoire, le 8086 envoyait l'adresse de l'opérande sur le bus, et le coprocesseur récupérait celle-ci sur le bus de données. Si l'opérande devait être lu en plusieurs fois, le coprocesseur lisait le reste de lui-même, en prenant le contrôle du bus d'adresse. Il récupérait l'adresse envoyée initialement par le CPU, puis l'incrémentait et relançait un nouvel accès mémoire. Il l'incrémentait autant de fois que nécessaire pour charger l'opérande. Un problème est que le CPU ne sait pas combien de temps dure une instruction x87. Et cette durée dépendait de l'implémentation du processeur, elle n'était pas la même selon la marque du coprocesseur. Un 186 n'avait pas les mêmes timings que le 286, par exemple. Pour le CPU, une instruction x87 met juste deux cycles pour s'exécuter (plus si des opérandes doivent être lus en mémoire). Pour cela, le CPU disposait d'un mécanisme de synchronisation. Le mécanisme de synchronisation était une instruction WAIT, qui forçait le CPU à attendre que le coprocesseur ait terminé l'instruction précédente. L'implémentation matérielle était assez simple. Le coprocesseur disposait d'une sortie BUSY, qui indiquait qu'il était en train d'exécuter une instruction et ne pouvait pas en accepter une nouvelle. Le CPU, quant à lui, avait une entrée TEST qui vérifiait si le, coprocesseur était occupé ou non. La sortie BUSY était reliée à l'entrée TEST. L'instruction test vérifiait juste ce qu'il y avait sur l'entrée TEST. Tant qu'elle était à 1, le processeur attendait et ne chargeait pas de nouvelle instruction. Dès qu'elle passe à 0, l'exécution reprend. Il faut noter que l'instruction WAIT n'est nécessaire qu'entre deux instructions flottantes assez proches. Mais il est possible d'intercaler des instructions entières entre deux instructions flottantes. Le programme pouvait ainsi mixer instructions entières et flottantes, les instructions entières étant exécutées sur le 8086, les instructions flottantes sur le coprocesseur. Il y avait donc une possibilité de parallélisme, à savoir que les deux processeurs pouvaient exécuter des instructions différentes en même temps. Mais cela demandait que les calculs soient coopératifs et mélangent bien entiers et flottants. Le 8087 et ses successeurs avaient une microarchitecture assez simple. L'unité de contrôle contenait un décodeur d'instruction microcodé, le registre de contrôle, le registre d'état. La plupart des instructions sont microcodées, l'unité de calcul est assez limitée. Elle permet d'additionner deux mantisses flottantes, de faire des décalages, d'additionner deux exposants, mais pas plus. Les multiplications et divisions sont donc microcodées et émulées en enchainant des additions flottantes. Les instructions trigonométriques sont implémentées en utilisant l'algorithme CORDIC, qu'on a vu dans le chapitre sur les circuits de calcul flottant. [[File:Intel 8087 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 8087.]] Le chemin de données est composé d'un banc de registre flottant pour la pseudo-pile, et de plusieurs circuits de calcul. Le banc de registre était mono-port, ce qui fait que les ALUs étaient précédés par deux registres temporaires pour les opérandes. Les circuits pour l'exposant et la mantisse sont séparés, et sont même reliés au banc de registre par deux bus séparés. Il y a un additionneur pour les exposants, un additionneur pour les mantisses et un décaleur pour les mantisses (pour les normaliser). De plus, on trouve une mémoire ROM dédiée aux constantes les plus utilisées. Elle sert pour les constantes de base, gérées par le jeu d'extension x87. Mais elle contient aussi des constantes utilisées pour l'algorithme CORDIC. Elle n'est pas illustrée sur le schéma ci-dessous, mais elle existe. L'interface avec le bus est un simple registre d’interfaçage avec le bus. Pour rappel, le bus de données fait 16 bits sur le 8087, 32 bits sur le 387. Entre le bus et le chemin de données, on trouve une file servant à simplifier la gestion des lectures. L'idée est que les opérandes lus/écrits font 32, 64 ou 80 bits, alors que le bus de données n'en fait que 16/32. Les opérandes sont donc lus/écrits en plusieurs passes. Sur le 8087, il doit réaliser deux passes pour des opérandes de 32 bits, quatre passes pour celles de 64 bits, 5 pour des opérandes de 80 bits (80 = 5 × 16). Le 387 doit faire deux fois moins. [[File:Intel 387 arch.svg|centre|vignette|upright=2.5|Microarchitecture de l'Intel 387. Les circuits pour les exposants sont à gauche dans le chemin de données, les circuits pour les mantisses sont à droite.]] L'implémentation du banc de registre est assez simple : une RAM avec un registre qui indique la position du sommet de la pile dedans. Le registre fait 3 bits, pour 8 registres. En plus de cela, il y a un petit soustracteur et un multiplexeur, pour adresser les opérandes dans la pile. Pour rappel, il est possible d'adresser la seconde opérande dans la pile. Mais on précise pas le numéro du registre dans la pile pour cela, on précise sa position sous le sommet de la pile, à savoir si elle est deux, trois, quatre opérandes sous le sommet de la pile. Pour déterminer quel registre lire, il faut soustraire ce "décalage" au numéro de registre du sommet de la pile. Pour cela, il y a un petit soustracteur pour faire le calcul. Le circuit décaleur est composé de deux sous-décaleurs. Le premier fait des décalages au niveau des octets, le second décale l'opérande de 0 à 7 rangs. Pour finir, voici quelques liens sur la microarchitecture du 8087 : * [https://www.righto.com/2026/02/8087-instruction-decoding.html Instruction decoding in the Intel 8087 floating-point chip]. * [https://www.righto.com/2020/05/die-analysis-of-8087-math-coprocessors.html Die analysis of the 8087 math coprocessor's fast bit shifter]. * [https://www.righto.com/2025/12/8087-stack-circuitry.html The stack circuitry of the Intel 8087 floating point chip, reverse-engineered]. * [https://www.righto.com/2025/12/8087-microcode-conditions.html Conditions in the Intel 8087 floating-point chip's microcode]. * [https://www.righto.com/2020/05/extracting-rom-constants-from-8087-math.html Extracting ROM constants from the 8087 math coprocessor's die]. * [https://www.righto.com/2018/09/two-bits-per-transistor-high-density.html Two bits per transistor: high-density ROM in Intel's 8087 floating point chip]. ===Les coprocesseurs x87 de Weitek=== Intel n'a pas été le seul fabricant à commercialiser des coprocesseurs x87. Weitek et de nombreuses autres entreprises s'y sont mises. Weitek a commercialisé plusieurs coprocesseurs : un premier coprocesseur appelé le 1067, le 1167, le 2167, le 3167 et le 4167. Ils sont tous rétrocompatibles entre eux, à savoir que le 4167 ne fait qu'ajouter des fonctionnalités au 3167, qui lui-même ajoute des fonctionnalités au 2167, et ainsi de suite. Ils gèrent tous les quatre opérations de base, ainsi que le calcul de la racine carrée. Le '''Weitek 1067''' avait pour particularité d'être fourni en pièces détachées, avec trois circuits séparés : un circuit de contrôle, une ALU flottante, et un multiplieur/diviseur. Ces trois pièces détachées étaient censées être soudées sur la carte mère. Le '''Weitek 1167''' regroupait ces trois pièces détachées sur une carte d'extension ISA. Par la suite, le '''Weitek 2167''' regroupa les trois pièces détachées dans un seul circuit imprimé. Il n'y avait pas de registres flottants adressables, ni de pile de registres. À la place, l'ALU et le multiplieur/diviseur intégraient deux registres pour les opérandes et un registre accumulateur. Les opérandes étaient présentés sur le bus de données et l'ALU les mémorisait dans deux registres internes de 64 bits chacun, nommés A et B. Le coprocesseur lisait les opérandes depuis ces deux registres et mémorisait le résultat dans un registre de résultat interne. Il envoyait alors le résultat sur le bus de données, prévenait le CPU avec une interruption, et le CPU récupérait le résultat sur le bus. Il faut noter que toutes les communications avec l'ALU passent par le bus de données, appelé le bus X. La transmission d'un flottant 32 bits se faisait en un cycle d'horloge, vu que le bus était de 32 bits. Par contre, la transmission d'un flottant 64 bits se faisait en deux cycles. À part le bus de données, il y avait un bus de commande, relié à l'unité de contrôle 1163. Elle envoyait l'opcode sur une entrée dédiée, notée F. Les bits de commande L, CSL, CSUS, CUSX, U commandent la lecture des opérandes ou leur écriture. L'unité de contrôle recevait le registre d'état via une sortie dédiée nommée S ou STATUS. [[File:Weitek WTL1167 arch.svg|centre|vignette|upright=2.5|Weitek WTL1167.]] Pour charger les opérandes dans l'ALU, celle-ci intégrait diverses entrées de commande nommées L0, L1, L2, L3 et CSL. * Le signal CSL est le ''Chip Select Load'', ce qui indique qu'il est mis à 1 lors d'une lecture. * Le bit L0 à 1 indique qu'un opcode est envoyé sur l'entrée F, l'entrée pour l'opcode est recopiée dans un registre interne, qui n'est pas un registre d'instruction vu que l'ALU n'a pas de décodeur. * Les bits L1 et L2 indiquent si ce qu'il y a sur le bus est : un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. * Le registre L3 indique quel est le registre de destination : A ou B. Les signaux U, CSUS et CSUX servaient pour l'envoi du résultat sur le bus. Ils précisaient s'il fallait copier un flottant 32 bits, les 32 bits de poids fort d'un flottant 64 bits, les 32 bits de poids faible d'un flottant 64 bits. Le '''Weitek 3167''' était conçu pour fonctionner en tandem avec un CPU Intel 386, alors que le '''Weitek 4167''' était prévu pour aller avec un Intel 486, mais ils fonctionnaient de la même manière au-delà de quelques détails. Contrairement à leurs prédécesseurs, ils intégraient 32 registres flottants de 32 bits. Un registre pair et un registre impair pouvaient être concaténés pour mémoriser un opérande 64 bits. Les opérations se faisaient soit entre deux registres flottants, soit entre un registre flottant et l'opérande présentée sur le bus de données. Le résultat est stocké dans un registre flottant. Leur registre de status contenait les résultats des comparaisons flottantes, ainsi que 8 bits pour les exceptions flottantes. Le registre de contrôle avait un champ pour configurer les arrondis, mais aussi un masque d'exceptions de 8 bits disant quels bits d'exceptions ignorer dans le registre de statut. Le coprocesseur est mappé en mémoire, ce qui fait qu'il a des adresses réservées dans lesquelles le processeur peut lire/écrire, pour lui envoyer une instruction, envoyer un opérande ou récupérer un résultat. Les adresses en question sont les adresses COOO OOOOh à COOO FFFFh, ce qui fait que le coprocesseur est adressé si les 16 bits de poids fort de l'adresse valent COOO. Par contre, les 65536 adresses réservées n'étaient pas associées à de la mémoire, pas même aux registres. A la place, les 16 bits de poids faible de l'adresse encodaient une instruction au coprocesseur, à savoir un opcode de 6 bits deux numéros de registres. Le processeur communique avec le coprocesseur en envoyant l'instruction sur le bus d'adresse, et éventuellement un opérande sur le bus de données. L'opérande vient généralement des registres, par simplicité, car cela permet de tout envoyer en une seule écriture. Par exemple, une écriture du registre EAX à l'adresse COOO OOOOh va copier le registre EAX sur le bus de données, et envoyer l'opcode de l'addition (0000) sur le bus d'adresse avec deux numéros de registre. Le coprocesseur fait alors une addition entre le registre flottant sélectionné, et l'opérande sur le bus de données copiée depuis EAX. ==Le Motorola 68881 et le 68882== Le 68881 de Motorola était conçu pour fonctionner avec les CPU 68020 et 68030. Les programmes mixaient instructions entières et flottantes, le 68000 exécutant les instructions entières, le 68881 exécutant les instructions flottantes. Les instructions flottantes avaient un opcode qui commençait par F (en hexadécimal), ce qui permettait de les distinguer rapidement du reste. Le 68000 chargeait les instructions, et regardait si l'instruction était destinée soit au coprocesseur, soit pour lui. Pour une instruction coprocesseur, il lisait les opérandes en RAM, puis envoyait instruction et opérandes au 68881. Il continuait son travail dans son coin, et récupérait le résultat quelques cycles plus tard. Malgré le fait qu'il y ait des instructions destinées au coprocesseur, le 68881 n'était pas un coprocesseur fortement couplé. A la place, le 68881 était géré comme une entrée-sortie, du point de vue du CPU. Il était mappé en mémoire, avait une entrée ''Chip Select'' commandé par décodage d'adresse. Il contenait aussi des registres d'interface, appelés des ''coprocessor interface registers'' (CIRs). Il y avait des registres pour l'opcode de l'instruction à exécuter, un autre pour chaque opérande, etc. Pour envoyer une instruction au 68881, le CPU avait juste à écrire dans les registres adéquats, idem pour charger les opérandes de l'instruction si besoin. Les coprocesseurs Motorola utilisaient des flottants codés sur 80 bits : la mantisse était codée sur 64 bits, l'exposant sur 15 bits. Le 68881 incorporait 8 registres flottants, nommés, de 80 bits chacun. Fait étonnant, cela ressemble beaucoup à ce qui est fait avec les coprocesseurs x87 : usage de flottants codés sur 80 bits, 8 registres flottants. Mais les détails sont différents, le jeu d'instruction est complétement différent. Les coprocesseurs Motorola avaient aussi un registre de statut et un registre de contrôle, guère plus. Le registre de statut mémorisait les conditions classiques, mais aussi des bits pour les exceptions qui ont été levées lors d'un calcul. Le registre de contrôle mémorisait de quoi configurer les arrondis, mais aussi un masque pour indiquer quelles exceptions ignorer dans le registre de statut. Pareil que pour les processeurs précédents, donc. <noinclude> {{NavChapitre | book=Fonctionnement d'un ordinateur | prev=Un exemple de jeu d'instruction : l'extension x87 | prevText=Un exemple de jeu d'instruction : l'extension x87 | next=L'accélération matérielle de la virtualisation | nextText=L'accélération matérielle de la virtualisation }} </noinclude> 7cfhmnm12hwpxmfyfgmebcnuvovlzdr Fonctionnement d'un ordinateur/Le contrôleur de périphériques 0 83806 763753 2026-04-16T13:32:18Z Mewtow 31375 Mewtow a déplacé la page [[Fonctionnement d'un ordinateur/Le contrôleur de périphériques]] vers [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements]] : Réutilisation d'un chapitre vide, vers un nouveau sujet 763753 wikitext text/x-wiki #REDIRECTION [[Fonctionnement d'un ordinateur/L'implémentation matérielle des branchements]] py1k4atd1kzf4pm0bemyrs39rto64cu Les cartes graphiques/Les écritures en VRAM hors ROPs 0 83807 763811 2026-04-16T20:24:19Z Mewtow 31375 Page créée avec « Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu diff... » 763811 wikitext text/x-wiki Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu différé qui ne fonctionneraient pas sans cela ! Et ne parlons pas des ''compute shaders'', qui ne serviraient à rien sans cela. Dans ce chapitre, nous allons détailler comment se font ces écritures en mémoire. : Notons que nous mettons de côté les architectures ''sort-middle'' et les GPU à rendu en ''tile'' dans ce chapitre. ==La fonctionnalité de ''stream output''== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] 0zz2utxs7czefvasfsa9zc52y0uto6q 763814 763811 2026-04-16T20:25:24Z Mewtow 31375 /* La fonctionnalité de stream output */ 763814 wikitext text/x-wiki Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu différé qui ne fonctionneraient pas sans cela ! Et ne parlons pas des ''compute shaders'', qui ne serviraient à rien sans cela. Dans ce chapitre, nous allons détailler comment se font ces écritures en mémoire. : Notons que nous mettons de côté les architectures ''sort-middle'' et les GPU à rendu en ''tile'' dans ce chapitre. ==La fonctionnalité de ''stream output''== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==Les fonctionnalités de ''Multiple Render Targets''== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. L'implémentation classique est que le ''pixel shader'' peut écrire son résultat dans une ou plusieurs textures. Précisons qu'il s'agit bien du ''pixel shader'' qui écrit dans une texture. Suivant le GPU, l'écriture dans les ''render target'' peut ou non passer par les ROPs. Il arrive que l'écriture dans un ROP passe par les ROPs, mais pas les autres. D'autres GPU acceptent de faire passer tous les ''render target'' par les ROPs, et mêrme de configurer le mélange ''alpyha'' différemment suivant le ''render target''. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] Le MRT accélère fortement les techniques de '''rendu différé''', qui enregistrent plusieurs images séparées, qui sont combinées par un '''pixel shader''' pour obtenir l'image finale. L'intérêt du rendu différé est d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} {{NavChapitre | book=Les cartes graphiques | prev=Les Render Output Target | prevText=Les Render Output Target | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} hrb9mfkrkkswf03zbf1khi9afvwod1r 763818 763814 2026-04-16T20:32:16Z Mewtow 31375 /* Les fonctionnalités de Multiple Render Targets */ 763818 wikitext text/x-wiki Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu différé qui ne fonctionneraient pas sans cela ! Et ne parlons pas des ''compute shaders'', qui ne serviraient à rien sans cela. Dans ce chapitre, nous allons détailler comment se font ces écritures en mémoire. : Notons que nous mettons de côté les architectures ''sort-middle'' et les GPU à rendu en ''tile'' dans ce chapitre. ==La fonctionnalité de ''stream output''== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==Les fonctionnalités de ''Multiple Render Targets''== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. L'implémentation classique est que le ''pixel shader'' peut écrire son résultat dans une ou plusieurs textures. Précisons qu'il s'agit bien du ''pixel shader'' qui écrit dans une texture. Suivant le GPU, l'écriture dans les ''render target'' peut ou non passer par les ROPs. Sur les anciennes versions de DirectX, un seul ''render target'' passait par les ROPs, mais pas les autres. De nos jours, tous les ''render target'' passent par les ROPs. Il est même possible de configurer le mélange ''alpha'' différemment suivant le ''render target''. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] Le MRT accélère fortement les techniques de '''rendu différé''', qui enregistrent plusieurs images séparées, qui sont combinées par un '''pixel shader''' pour obtenir l'image finale. L'intérêt du rendu différé est d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Un objet éclairé par N sources de lumière demandait N ''draw call'' pour être éclairé. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage n'est pas réalisé sur des pixels invisibles, à savoir des calculer l'éclairage pour des triangles cachés par un objet opaque. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} {{NavChapitre | book=Les cartes graphiques | prev=Les Render Output Target | prevText=Les Render Output Target | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} 1udyw1cwz3i0hutiuk7l74aai0en1ma 763819 763818 2026-04-16T20:34:46Z Mewtow 31375 /* Les fonctionnalités de Multiple Render Targets */ 763819 wikitext text/x-wiki Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu différé qui ne fonctionneraient pas sans cela ! Et ne parlons pas des ''compute shaders'', qui ne serviraient à rien sans cela. Dans ce chapitre, nous allons détailler comment se font ces écritures en mémoire. : Notons que nous mettons de côté les architectures ''sort-middle'' et les GPU à rendu en ''tile'' dans ce chapitre. ==La fonctionnalité de ''stream output''== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==Les fonctionnalités de ''Multiple Render Targets''== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. L'implémentation classique est que le ''pixel shader'' peut écrire son résultat dans une ou plusieurs textures. Précisons qu'il s'agit bien du ''pixel shader'' qui écrit dans une texture. Suivant le GPU, l'écriture dans les ''render target'' peut ou non passer par les ROPs. Sur les anciennes versions de DirectX, un seul ''render target'' passait par les ROPs, mais pas les autres. De nos jours, tous les ''render target'' passent par les ROPs. Il est même possible de configurer le mélange ''alpha'' différemment suivant le ''render target''. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] Le MRT accélère fortement les techniques de '''rendu différé''', qui enregistrent plusieurs images séparées, qui sont combinées par un '''pixel shader''' pour obtenir l'image finale. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures : une avec la couleur non-éclairée de chaque pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une texture contenant les normales de la surface pour chaque pixel, et une texture pour d'autres informations (couleur spéculaire, autres). Les textures sont ensuite utilisées par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture", appelés des '''''G-buffer''''', pour gérer de telles techniques. De plus, le MRT optimise le rendu. Pas besoin de faire un ''draw call'' par ''G-buffer'', chacun recalculant la géométrie. Avec le MRT, les différents ''G-buffer'' sont calculés en une seule passe, la géométrie n'est calculée qu'une seule fois. {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} L'intérêt du rendu différé est d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Pour traiter M objets éclairés par N sources de lumière, il fallait faire M * N ''draw call'' pour être éclairés. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage est réalisé une fois par source de lumière, pour chaque pixel, pas M fois. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. {{NavChapitre | book=Les cartes graphiques | prev=Les Render Output Target | prevText=Les Render Output Target | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} cuwu7q2dybg3l7gk2wio3n7u903u0uk 763820 763819 2026-04-16T20:45:37Z Mewtow 31375 /* Les fonctionnalités de Multiple Render Targets */ 763820 wikitext text/x-wiki Le chapitre précédent nous a expliqué comment les ROPs mettaient à jour le ''framebuffer'' et le tampon de profondeur. Cependant, les ROPs ne sont pas les seuls à écrire en mémoire vidéo. Les ''compute shaders'', ''vertex shaders'' et ''pixel shaders'' peuvent écrire des données en mémoire vidéo, dans des tableaux dédiés. Et n'allez pas croire que c'est un petit sujet. La majeure partie des jeux vidéos actuels utilisent des techniques de rendu différé qui ne fonctionneraient pas sans cela ! Et ne parlons pas des ''compute shaders'', qui ne serviraient à rien sans cela. Dans ce chapitre, nous allons détailler comment se font ces écritures en mémoire. : Notons que nous mettons de côté les architectures ''sort-middle'' et les GPU à rendu en ''tile'' dans ce chapitre. ==La fonctionnalité de ''stream output''== Une fonctionnalité des ''geometry shaders'' est la possibilité d'enregistrer leurs résultats en mémoire. Il s'agit de la fonctionnalité du '''''stream output'''''. On peut ainsi remplir une texture ou le ''vertex buffer'' dans la mémoire vidéo, avec le résultat d'un ''geometry shader''. Notons que celle-ci mémorise un ensemble de primitives, pas autre chose. Cette fonctionnalité est utilisée pour certains effets ou rendu bien précis, mais il faut avouer qu'elle n'est pas très souvent utilisée. Aussi, les concepteurs de cartes graphiques n'ont pas optimisé cette fonctionnalité au maximum. Le ''stream output'' n'a généralement pas accès prioritaire à la mémoire, comparé aux ROP, et n'a souvent accès qu'à une partie limitée de la bande passante mémoire. Notons qu'il existe deux formes de ''stream output'' : une qui permet aux ''vertex shader'' d'écrire dans une texture, l'autre qui permet aux ''geometry shaders'' de le faire. Notons que le ''stream output'' fournit un flux de primitives, pas de sommets, même pour le flux sortant d'un ''vertex shader''. En clair, beaucoup de sommets sont dupliqués et ont n'a pas d{{'}}''index buffer''. Les résultats du ''stream output'' sont donc assez lourds et prennent beaucoup de mémoire. [[File:Stream output.png|centre|vignette|upright=2.5|Stream output]] ==Les fonctionnalités de ''Multiple Render Targets''== Il faut noter que les API modernes permettent à un ''pixel shader'' d'écrire dans plusieurs ''render-target''. On parle alors de '''''Multiple Render Targets''''', abrévié en MRT. L'implémentation classique est que le ''pixel shader'' peut écrire son résultat dans une ou plusieurs textures. Précisons qu'il s'agit bien du ''pixel shader'' qui écrit dans une texture. Suivant le GPU, l'écriture dans les ''render target'' peut ou non passer par les ROPs. Sur les anciennes versions de DirectX, un seul ''render target'' passait par les ROPs, mais pas les autres. De nos jours, tous les ''render target'' passent par les ROPs. Il est même possible de configurer le mélange ''alpha'' différemment suivant le ''render target''. [[File:MultiRenderTarget.svg|centre|vignette|upright=3|''Multiple Render Target''.]] Le MRT accélère fortement les techniques de '''rendu différé''', qui enregistrent plusieurs images séparées, qui sont combinées par un '''pixel shader''' pour obtenir l'image finale. Le rendu différé demande deux passes de rendu. La première passe calcule tout, sauf le ''pixel shader'', il n'y a pas de calculs d'éclairage par pixel. Elle enregistre son résultat dans plusieurs textures, au minimum trois : une avec la couleur du pixel, une autre pour la profondeur de chaque pixel (le tampon de profondeur), une autre pour les normales de la surface pour chaque pixel. Il est possible d'ajouter d'autres textures pour d'autres informations, comme des textures pour les paramètres de la lumière spéculaire (couleur spéculaire, illumination). La seconde passe utilise par un ''pixel shader'' pour calculer l'image finale avec éclairage. Il faut alors supporter des pseudo-''framebuffer'' pour chaque "texture". Ils sont appelés des '''''G-buffer''''', ce qui est l'abréviation de tampon géométrique. Il est possible de fusionner des ''g-buffer'', pour gagner de la place. Par exemple, la couleur diffuse est une couleur RGB codée sur 24 bits, là où une texture utilise souvent des pixels RGBA codés sur 32 bits. Il est alors d'utiliser l'octet pour la composante A pour stocker une autre information, comme une information de métallicité (utilisée pour calculer la couleur spéculaire). {| |[[File:Deferred rendering pass col.jpg|thumb|''G-buffer'' pour la couleur.]] |[[File:Deferred rendering pass dep.jpg|thumb|''G-buffer'' pour la profondeur.]] |[[File:Deferred rendering pass nor.jpg|thumb|''G-buffer'' pour les normales.]] |[[File:Deferred rendering pass res.jpg|thumb|Image finale]] |} Sans MRT, le rendu différé est très compliqué à implémenter. Chaque ''g-buffer'' demande une passe de rendu, chacune re-calculant la géométrie. Par contre, avec MRT, les différents ''g-buffer'' sont tous calculés en une seule passe, sans recalculer la géométrie plusieurs fois. L'intérêt du rendu différé est d'accélérer le calcul de l'éclairage par pixel. Sans rendu différé, avec les anciennes API graphiques, il fallait utiliser un ''draw call'' par objet et par source de lumière. Pour traiter M objets éclairés par N sources de lumière, il fallait faire M * N ''draw call'' pour être éclairés. Avec le rendu différé, pas besoin. De plus, on garantit que le calcul de l'éclairage est réalisé une fois par source de lumière, pour chaque pixel, pas M fois. Le désavantage est que la transparence n'est pas prise en charge, de même que l’antialiasing de type MSAA. {{NavChapitre | book=Les cartes graphiques | prev=Les Render Output Target | prevText=Les Render Output Target | next=Le support matériel du lancer de rayons | nextText=Le support matériel du lancer de rayons }}{{autocat}} c7s1l875b9dgx18bf6t4cwosvitw78p Mathc initiation/0068 0 83808 763832 2026-04-16T21:58:30Z Xhungab 23827 news 763832 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|Calcul de le Rotationnel (Curl) au point p}} : L'opérateur rotationnel est un opérateur différentiel aux dérivées partielles qui, à un champ vectoriel tridimensionnel, noté A, fait correspondre un autre champ Plus difficile à se représenter aussi précisément que le gradient et la divergence, il exprime la tendance qu'ont les lignes de champ d'un champ vectoriel à tourner autour d'un point : sa circulation locale sur un petit lacet entourant ce point est non nulle quand son rotationnel ne l'est pas. [https://fr.wikipedia.org/wiki/Rotationnel Wikipédia] : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/0063|x_afile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c26a4|x_fxyz.h ............ Calculer les dérivées partielles]] * [[Mathc initiation/0064|x_curl.h .............. Calculer la divergence au point p]] : . : Les fonctions pour les différents exemples : * [[Mathc initiation/0065|f.h]] : . : '''Calculer curl_ijk(); au point p :''' * [[Mathc initiation/0066|c00a.c ]] '''Calculer Ncurl_ijk(); au point p :''' * [[Mathc initiation/0067|c00b.c ]] {{AutoCat}} 06blr9lp2nkmyfsaq7myje9am4m4ov2 763835 763832 2026-04-16T22:07:42Z Xhungab 23827 763835 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|Calcul de le Rotationnel (Curl) au point p}} : L'opérateur rotationnel est un opérateur différentiel aux dérivées partielles qui, à un champ vectoriel tridimensionnel, noté A, fait correspondre un autre champ Plus difficile à se représenter aussi précisément que le gradient et la divergence, il exprime la tendance qu'ont les lignes de champ d'un champ vectoriel à tourner autour d'un point : sa circulation locale sur un petit lacet entourant ce point est non nulle quand son rotationnel ne l'est pas. [https://fr.wikipedia.org/wiki/Rotationnel Wikipédia] : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/0063|x_afile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c26a4|x_fxyz.h ............ Calculer les dérivées partielles]] * [[Mathc initiation/0064|x_curl.h .............. Calculer le rotationnel au point p]] : . : Les fonctions pour les différents exemples : * [[Mathc initiation/0065|f.h]] : . : '''Calculer curl_ijk(); au point p :''' * [[Mathc initiation/0066|c00a.c ]] '''Calculer Ncurl_ijk(); au point p :''' * [[Mathc initiation/0067|c00b.c ]] {{AutoCat}} 2kudt50nuy428boct42xce2h1exyv5z 763861 763835 2026-04-17T11:43:14Z Xhungab 23827 763861 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|Calcul de le Rotationnel (Curl) au point p}} : L'opérateur rotationnel est un opérateur différentiel aux dérivées partielles qui, à un champ vectoriel tridimensionnel, noté A, fait correspondre un autre champ Plus difficile à se représenter aussi précisément que le gradient et la divergence, il exprime la tendance qu'ont les lignes de champ d'un champ vectoriel à tourner autour d'un point : sa circulation locale sur un petit lacet entourant ce point est non nulle quand son rotationnel ne l'est pas. [https://fr.wikipedia.org/wiki/Rotationnel Wikipédia] : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/0063|x_afile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c26a4|x_fxyz.h ............ Calculer les dérivées partielles]] * [[Mathc initiation/0064|x_curl.h .............. Calculer le rotationnel au point p]] : . : Les fonctions pour les différents exemples : * [[Mathc initiation/0065|f.h]] : . : '''Calculer curl_ijk(); et Ncurl_ijk(); au point p :''' * [[Mathc initiation/0066|c00a.c ]] '''Calculer curl_ijk(); et Ncurl_ijk(); au point p :''' * [[Mathc initiation/0067|c00b.c ]] {{AutoCat}} nbws29f2fy5okau1z2z1zzo8c0c1i2w Mathc initiation/0063 0 83809 763833 2026-04-16T22:00:37Z Xhungab 23827 news 763833 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0068| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_afile.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_afile.h */ /* ---------------------------------- */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #include <math.h> #include <string.h> /* ---------------------------------- */ #include "x_def.h" #include "x_strcp.h" /* ---------------------------------- */ #include "x_fxyz.h" #include "x_curl.h" /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} 2lqc2wmie0gp9djzq5dc1jkplby08uv Mathc initiation/0064 0 83810 763834 2026-04-16T22:03:12Z Xhungab 23827 news 763834 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0068| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_curl.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_curl.h */ /* ---------------------------------- */ /* with F = Mi + Nj + Pk | i j k | | _x _y _z | | M N P | (curl F) = [(P_y-N_z)i + (M_y-P_z)j + (N_X-M_Y)k] */ /* ---------------------------------- */ v3d curl_ijk( double (*P_M)(double x, double y, double z), double (*P_N)(double x, double y, double z), double (*P_P)(double x, double y, double z), pt3d p ) { v3d curl; curl.i = fxyz_y((*P_P),H,p) - fxyz_z((*P_N),H,p); curl.j = fxyz_z((*P_M),H,p) - fxyz_x((*P_P),H,p); curl.k = fxyz_x((*P_N),H,p) - fxyz_y((*P_M),H,p); return(curl); } /* ---------------------------------- */ /* ---------------------------------- */ double Ncurl_ijk( double (*P_M)(double x, double y, double z), double (*P_N)(double x, double y, double z), double (*P_P)(double x, double y, double z), pt3d p ) { v3d curl; curl.i = fxyz_y((*P_P),H,p) - fxyz_z((*P_N),H,p); curl.j = fxyz_z((*P_M),H,p) - fxyz_x((*P_P),H,p); curl.k = fxyz_x((*P_N),H,p) - fxyz_y((*P_M),H,p); return(sqrt(curl.i*curl.i+ curl.j*curl.j+ curl.k*curl.k)); } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} ocotn5ulktumksgf7u8e15amj1nt5hl Mathc initiation/0066 0 83811 763836 2026-04-16T22:09:54Z Xhungab 23827 news 763836 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0068| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00a.c */ /* ---------------------------------- */ #include "x_afile.h" #include "fa.h" /* ---------------------------------- */ int main(void) { pt3d p3d = {1, 2, 3}; v3d curl = curl_ijk(M,N,P, p3d); clrscrn(); printf(" F(x,y,z) = %si %sj %sk \n\n",Meq,Neq,Peq); printf(" with F = Mi + Nj + Pk \n\n" " | i j k | \n" " | _x _y _z | \n" " | M N P | \n\n" " curl(F) = [(P_y-N_z)i + (M_z-P_x)j + (N_x-M_y)k] \n\n" " curl(F) = (%.3f,%.3f,%.3f)\n\n" " Compute the Norm of the curl:\n\n" " ||curl(F)|| = %.3f \n\n" , curl.i,curl.j,curl.k, Ncurl_ijk(M,N,P, p3d)); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> F(x,y,z) = + 3*z*yi + 4*xj + 2*y*xk with F = Mi + Nj + Pk | i j k | | _x _y _z | | M N P | curl(F) = [(P_y-N_z)i + (M_z-P_x)j + (N_x-M_y)k] curl(F) = (2.000,2.000,-5.000) Compute the Norm of the curl: ||curl(F)|| = 5.745 Press return to continue. </syntaxhighlight> {{AutoCat}} helz0o8mdl03p61kfgqb3lwdx11fxdt Mathc initiation/0067 0 83812 763837 2026-04-16T22:13:03Z Xhungab 23827 news 763837 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0068| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00b.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00b.c */ /* ---------------------------------- */ #include "x_afile.h" #include "fb.h" /* ---------------------------------- */ int main(void) { pt3d p3d = {1, 2, 3}; v3d curl = curl_ijk(M,N,P, p3d); clrscrn(); printf(" F(x,y,z) = %si %sj %sk \n\n",Meq,Neq,Peq); printf(" with F = Mi + Nj + Pk \n\n" " | i j k | \n" " | _x _y _z | \n" " | M N P | \n\n" " curl(F) = [(P_y-N_z)i + (M_z-P_x)j + (N_x-M_y)k] \n\n" " curl(F) = (%.3f,%.3f,%.3f)\n\n" " Compute the Norm of the curl:\n\n" " ||curl(F)|| = %.3f \n\n" , curl.i,curl.j,curl.k, Ncurl_ijk(M,N,P, p3d)); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> F(x,y,z) = + 2*yi + cos(z)*xj - sin(x)*yk with F = Mi + Nj + Pk | i j k | | _x _y _z | | M N P | curl(F) = [(P_y-N_z)i + (M_z-P_x)j + (N_x-M_y)k] curl(F) = (-0.700,1.081,-2.990) Compute the Norm of the curl: ||curl(F)|| = 3.255 Press return to continue. </syntaxhighlight> {{AutoCat}} 25masag8mjzg2yb63dvmf36jfj49imh Mathc initiation/0065 0 83813 763838 2026-04-16T22:15:39Z Xhungab 23827 news 763838 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0068| Sommaire]] Installer ce fichier dans votre répertoire de travail. {| class="wikitable" |+ Texte de la légende |- | {{Fichier|fa.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* --------------------------------- */ /* save as fa.h */ /* --------------------------------- */ double M( double x, double y, double z) { return( (3*z*y) ); } char Meq[] = "+ 3*z*y"; /* --------------------------------- */ double N( double x, double y, double z) { return( (4*x) ); } char Neq[] = "+ 4*x"; /* --------------------------------- */ double P( double x, double y, double z) { return( (2*y*x) ); } char Peq[] = " + 2*y*x"; /* --------------------------------- */ /* --------------------------------- */ </syntaxhighlight> || {{Fichier|fb.h |largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* --------------------------------- */ /* save as fb.h */ /* --------------------------------- */ double M( double x, double y, double z) { return( (2*y) ); } char Meq[] = "+ 2*y"; /* --------------------------------- */ double N( double x, double y, double z) { return( (cos(z)*x) ); } char Neq[] = "+ cos(z)*x"; /* --------------------------------- */ double P( double x, double y, double z) { return( -(sin(x)*y) ); } char Peq[] = " - sin(x)*y"; /* --------------------------------- */ /* --------------------------------- */ </syntaxhighlight> |} {{AutoCat}} m642ha7r0exgzxg81ail8ysrel5lsxh Mathc initiation/0069 0 83814 763863 2026-04-17T11:47:36Z Xhungab 23827 news 763863 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|Calcul de R_s_x_R_t au point p}} Se sera utilisé dans le calcul de l'intégrale de flux de surface définie paramétriquement, simplifiée. : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/0063|x_afile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/0064|x_rsxrt.h ........... Calcul de R_s_x_R_t au point p]] : . : Les fonctions pour les différents exemples : * [[Mathc initiation/0065|f.h]] : . : '''Calculer R_s_x_R_t au point p :''' * [[Mathc initiation/0066|c00a.c ]] '''Calculer R_s_x_R_t au point p :''' * [[Mathc initiation/0067|c00b.c ]] {{AutoCat}} jcub2o65hkmxuw4r0v0pp7v6ddjmgnr 763865 763863 2026-04-17T11:50:57Z Xhungab 23827 news 763865 wikitext text/x-wiki __NOTOC__ [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/Fichiers h : c44a4| Sommaire]] {{Partie{{{type|}}}|Calcul de R_s_x_R_t au point p}} Se sera utilisé dans le calcul de l'intégrale de flux de surface définie paramétriquement, simplifiée. : . : Copier la bibliothèque dans votre répertoire de travail : * [[Mathc initiation/006d|x_afile.h ............. Déclaration des fichiers h]] * [[Mathc initiation/Fichiers h : c30a2|x_def.h .............. Déclaration des utilitaires]] * [[Mathc initiation/Fichiers c : c47ca|x_strcp.h ........... Déclaration des structures (points, vecteurs)]] * [[Mathc initiation/Fichiers h : c25a4|x_fxy.h .............. Calculer les dérivées partielles]] * [[Mathc initiation/a452|x_rsxrt.h ............ Calcul de R_s_x_R_t au point p]] : . : Les fonctions pour les différents exemples : * [[Mathc initiation/006c|f.h]] : . : '''Calculer R_s_x_R_t au point p :''' * [[Mathc initiation/006b|c00a.c ]] '''Calculer R_s_x_R_t au point p :''' * [[Mathc initiation/006a|c00b.c ]] {{AutoCat}} hi9f8ky5wio4tfkl6qbpd3vnv59txpi Mathc initiation/006d 0 83815 763866 2026-04-17T11:52:42Z Xhungab 23827 news 763866 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0069| Sommaire]] Installer ce fichier dans votre répertoire de travail. {{Fichier|x_afile.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as x_afile.h */ /* ---------------------------------- */ #include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <time.h> #include <math.h> #include <string.h> /* ---------------------------------- */ #include "x_def.h" #include "x_strcp.h" /* ---------------------------------- */ #include "x_fxy.h" #include "x_rsxrt.h" /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> {{AutoCat}} 0tinsrx2p9rnx05h77uelwecktrhr2p Mathc initiation/006c 0 83816 763867 2026-04-17T11:55:20Z Xhungab 23827 news 763867 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0069| Sommaire]] Installer ce fichier dans votre répertoire de travail. {| class="wikitable" |+ Texte de la légende |- | {{Fichier|fa.h|largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as fa.h */ /* ---------------------------------- */ #define LOOP 2*100 /* ---------------------------------- */ double Rx( double s, double t) { return( (s) ); } char Rxeq[] = "+(s)"; /* ---------------------------------- */ double Ry( double s, double t) { return( (t*t) ); } char Ryeq[] = "+(t**2)"; /* ---------------------------------- */ double Rz( double s, double t) { return( (s*t) ); } char Rzeq[] = "+(s*t)"; /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> || {{Fichier|fb.h |largeur=70%|info=|icon=Crystal Clear mimetype source h.png}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as fb.h */ /* ---------------------------------- */ #define LOOP 2*100 /* ---------------------------------- */ double Rx( double s, double t) { return( (sin(s)*cos(t)) ); } char Rxeq[] = "+(sin(s)*cos(t))"; /* ---------------------------------- */ double Ry( double s, double t) { return( (sin(s)*sin(t)) ); } char Ryeq[] = "+(sin(s)*sin(t))"; /* ---------------------------------- */ double Rz( double s, double t) { return( (cos(s)) ); } char Rzeq[] = "+(cos(s))"; /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> |} {{AutoCat}} jxclcal6rkxu0ijrnw51zeb2m5it6te Mathc initiation/006b 0 83817 763869 2026-04-17T11:57:19Z Xhungab 23827 news 763869 wikitext text/x-wiki [[Catégorie:Mathc initiation (livre)]] [[Mathc initiation/0069| Sommaire]] Installer et compiler ces fichiers dans votre répertoire de travail. {{Fichier|c00a.c|largeur=70%|info=|icon=Crystal128-source-c.svg}} <syntaxhighlight lang="c"> /* ---------------------------------- */ /* save as c00a.c */ /* ---------------------------------- */ #include "x_afile.h" #include "fa.h" /* ---------------------------------- */ int main(void) { pt2d p = {1, 2}; v3d V = R_s_x_R_t(Rx,Ry,Rz, p); clrscrn(); printf(" R : (s,t)-> %si %sj %sk\n\n", Rxeq,Ryeq,Rzeq); printf(" with R = Rx i + Ry j + Rz k \n\n" " | i j k | \n" " | Rx_s Ry_s Rz_s | \n" " | Rx_t Ry_t Rz_t |\n\n" " R_s x R_t = (Ry_s * Rz_t - Ry_t * Rz_s) i \n" " - (Rx_s * Rz_t - Rx_t * Rz_s) j \n" " + (Rx_s * Ry_t - Rx_t * Ry_s) k \n\n" " R_s x R_t = (%.3f,%.3f,%.3f)\n\n" " Compute the Norm of R_s x R_t:\n\n" " ||R_s x R_t|| = %.3f \n\n" , V.i,V.j,V.k, NR_s_x_R_t(Rx,Ry,Rz, p)); stop(); return 0; } /* ---------------------------------- */ /* ---------------------------------- */ </syntaxhighlight> '''Exemple de sortie écran :''' <syntaxhighlight lang="C"> R : (s,t)-> +(s)i +(t**2)j +(s*t)k with R = Rx i + Ry j + Rz k | i j k | | Rx_s Ry_s Rz_s | | Rx_t Ry_t Rz_t | R_s x R_t = (Ry_s * Rz_t - Ry_t * Rz_s) i - (Rx_s * Rz_t - Rx_t * Rz_s) j + (Rx_s * Ry_t - Rx_t * Ry_s) k R_s x R_t = (-8.000,-1.000,4.000) Compute the Norm of R_s x R_t: ||R_s x R_t|| = 9.000 Press return to continue. </syntaxhighlight> {{AutoCat}} dlewuja54zs0878egvgalmmp908cidr