gma: Add Intel i945 (Gen3) graphics init support

Add i945G (desktop) and i945GM (mobile) generation support, modeled
after the existing G45 generation code with hardware-specific
adaptations based on the Linux i915 DRM driver and coreboot.

Key hardware differences from G45 (Gen4):
- GTT on separate PCI BAR3 (not within BAR0)
- Simple 32-bit GTT PTEs (addr[31:12] | valid[0])
- No DSPSURF register (uses DSPADDR/DSPLINOFF instead)
- Gen3 fence registers: 32-bit at split 0x2000/0x3000 addresses
- Different PLL limits (VCO 1400-2800 MHz, 96 MHz refclk)
- SDVO multiplier in DPLL register bits[7:4]
- LVDS restricted to Pipe B (pre-i965 requirement)
- CDClk: fixed 400 MHz (desktop) or GCFGC-based (mobile)
- No HDMI/DP, only VGA, LVDS, and SDVO outputs
- PCI IDs: 0x2772 (I945G), 0x27a2/0x27ae (I945GM)

TESTED with thinkpad x60: LVDS & VGA works with a linear framebuffer.

Change-Id: Ib67b3d0ee5e06df427869dce4db926ba57a80fd8
Signed-off-by: Arthur Heymans <arthur@aheymans.xyz>
Reviewed-on: https://review.sourcearcade.org/c/libgfxinit/+/476
Reviewed-by: Nico Huber <nico.h@gmx.de>
Reviewed-by: Angel Pons <th3fanbus@gmail.com>
Tested-by: Nico Huber <nico.h@gmx.de>
diff --git a/common/Makefile.inc b/common/Makefile.inc
index 906e1cd..47ef35b 100644
--- a/common/Makefile.inc
+++ b/common/Makefile.inc
@@ -62,14 +62,15 @@
 CONFIG_GFX_GMA_ANALOG_I2C_PORT	:= $(call strip_quotes,$(CONFIG_GFX_GMA_ANALOG_I2C_PORT))
 CONFIG_GFX_GMA_IGNORE_PRESENCE_STRAPS := $(if $(filter y,$(CONFIG_GFX_GMA_IGNORE_PRESENCE_STRAPS)),True,False)
 
-_GEN_TLA_SUBSTITUTIONS := g45 ilk hsw skl tgl
+_GEN_TLA_SUBSTITUTIONS := i945 g45 ilk hsw skl tgl
 
 _GEN_NONCONST := $(strip \
+		 $(if $(filter I945,$(CONFIG_GFX_GMA_GENERATION)),i945, \
 		 $(if $(filter G45,$(CONFIG_GFX_GMA_GENERATION)),g45, \
 		 $(if $(filter Ironlake,$(CONFIG_GFX_GMA_GENERATION)),ilk, \
 		 $(if $(filter Haswell,$(CONFIG_GFX_GMA_GENERATION)),hsw,  \
 		 $(if $(filter Skylake,$(CONFIG_GFX_GMA_GENERATION)),skl, \
-		 $(if $(filter Tigerlake,$(CONFIG_GFX_GMA_GENERATION)),tgl))))))
+		 $(if $(filter Tigerlake,$(CONFIG_GFX_GMA_GENERATION)),tgl)))))))
 # GNATprove (GPL 2017) doesn't realize when a boolean expression
 # that depends both on static values and variables can be evalu-
 # ated at compile time (e.g. `False and then Variable` is always
@@ -121,7 +122,9 @@
 gfxinit-gen-y += $(hw-gfx-gma-config-ads)
 gfxinit-$(CONFIG_GFX_GMA_DYN_CPU) += dyncpu/hw-gfx-gma-config.adb
 
-ifneq ($(filter G45,$(CONFIG_GFX_GMA_GENERATION)),)
+ifneq ($(filter I945,$(CONFIG_GFX_GMA_GENERATION)),)
+subdirs-y += i945
+else ifneq ($(filter G45,$(CONFIG_GFX_GMA_GENERATION)),)
 subdirs-y += g45
 else ifneq ($(filter Ironlake,$(CONFIG_GFX_GMA_GENERATION)),)
 subdirs-y += ironlake
diff --git a/common/hw-gfx-gma-config.ads.template b/common/hw-gfx-gma-config.ads.template
index dd6c2f8..2c859c5 100644
--- a/common/hw-gfx-gma-config.ads.template
+++ b/common/hw-gfx-gma-config.ads.template
@@ -18,6 +18,7 @@
 
    CPU_First : constant CPU_Type :=
      (case Gen is
+         when I945      => I945G,
          when G45       => G45,
          when Ironlake  => Ironlake,
          when Haswell   => Haswell,
@@ -26,6 +27,7 @@
          when Tigerlake => Tigerlake);
    CPU_Last : constant CPU_Type :=
      (case Gen is
+         when I945      => I945GM,
          when G45       => GM45,
          when Ironlake  => Ivybridge,
          when Haswell   => Broadwell,
@@ -45,6 +47,7 @@
 
    PCH_First : constant PCH_Type :=
      (case Gen is
+         when I945      => No_PCH,
          when G45       => No_PCH,
          when Ironlake  => Ibex_Peak,
          when Haswell   => Lynx_Point,
@@ -53,6 +56,7 @@
          when Tigerlake => Tiger_Point);
    PCH_Last : constant PCH_Type :=
      (case Gen is
+         when I945      => No_PCH,
          when G45       => No_PCH,
          when Ironlake  => Cougar_Point,
          when Haswell   => Lynx_Point,
@@ -85,8 +89,8 @@
    -- the CDClk rate. To ease proofs, we limit CDClk's range.
    CDClk_Min : constant Frequency_Type :=
      (case Gen is
-         when G45 .. Ironlake => Frequency_Type'First * 100 / 90 + 1,
-         when others          => Frequency_Type'First);
+         when I945 .. Ironlake => Frequency_Type'First * 100 / 90 + 1,
+         when others           => Frequency_Type'First);
    subtype CDClk_Range is Frequency_Type range CDClk_Min .. Frequency_Type'Last;
 
    ----------------------------------------------------------------------------
@@ -137,6 +141,7 @@
    -- To ease parsing, all multiline expressions of tagged config
    -- values start after a line break.
 
+   Gen_I945          : <genbool> := Gen = I945;
    Gen_G45           : <genbool> := Gen = G45;
    Gen_Ironlake      : <genbool> := Gen = Ironlake;
    Gen_Haswell       : <genbool> := Gen = Haswell;
@@ -144,13 +149,16 @@
    Gen_Skylake       : <genbool> := Gen = Skylake;
    Gen_Tigerlake     : <genbool> := Gen = Tigerlake;
 
+   Up_To_G45         : <genbool> := Gen <= G45;
    Up_To_Ironlake    : <genbool> := Gen <= Ironlake;
+   G45_On            : <genbool> := Gen >= G45;
    Ironlake_On       : <genbool> := Gen >= Ironlake;
    Haswell_On        : <genbool> := Gen >= Haswell;
    Broxton_On        : <genbool> := Gen >= Broxton;
    Skylake_On        : <genbool> := Gen >= Skylake;
    Tigerlake_On      : <genbool> := Gen >= Tigerlake;
 
+   GMCH_I945GM       : <i945bool> := Gen_I945 and then CPU = I945GM;
    GMCH_GM45         : <g45bool> := Gen_G45 and then CPU = GM45;
    CPU_Ironlake      : <ilkbool> := Gen_Ironlake and then CPU = Ironlake;
    CPU_Sandybridge   : <ilkbool> := Gen_Ironlake and then CPU = Sandybridge;
@@ -185,7 +193,7 @@
 
    Have_DVI_I              : constant Boolean := Analog_I2C_Port /= PCH_DAC;
 
-   Has_Presence_Straps           : <genbool> := not Gen_Broxton;
+   Has_Presence_Straps           : <genbool> := not Gen_Broxton and not Gen_I945;
    Is_ULT                        : <hswskltglbool> :=
      ((Gen_Haswell or Gen_Skylake or Gen_Tigerlake) and then CPU_Var = ULT);
    Is_ULX                        : <hswskltglbool> :=
@@ -194,7 +202,7 @@
 
    ---------- CPU pipe: ---------
    Has_Tertiary_Pipe             : <ilkbool> := Ivybridge_On;
-   Disable_Trickle_Feed          : <genbool> := not Gen_Haswell;
+   Disable_Trickle_Feed          : <genbool> := not Gen_Haswell and not Gen_I945;
    Pipe_Enabled_Workaround       : <hswbool> := CPU_Broadwell;
    Has_EDP_Transcoder            : <genbool> := Haswell_On and not Tigerlake_On;
    Use_PDW_For_EDP_Scaling       : <hswbool> := CPU_Haswell;
@@ -205,12 +213,13 @@
    Has_Pipeconf_BPC              : <hswbool> := not CPU_Haswell;
    Has_Plane_Control             : <genbool> := Broxton_On;
    Has_DSP_Linoff                : <genbool> := Up_To_Ironlake;
+   Has_DSPSURF                   : <genbool> := G45_On;
    Has_PF_Pipe_Select            : <ilkhswbool> := CPU_Ivybridge or CPU_Haswell;
    Has_Ivybridge_Cursors         : <ilkbool> := Ivybridge_On;
    VGA_Plane_Workaround          : <ilkbool> := CPU_Ivybridge;
    Has_GMCH_DP_Transcoder        : <genbool> := Gen_G45;
-   Has_GMCH_VGACNTRL             : <genbool> := Gen_G45;
-   Has_GMCH_PFIT_CONTROL         : <genbool> := Gen_G45;
+   Has_GMCH_VGACNTRL             : <genbool> := Up_To_G45;
+   Has_GMCH_PFIT_CONTROL         : <genbool> := Up_To_G45;
 
    ----------- Transcoder -------
    Need_Early_Transcoder_Setup   : <genbool> := Tigerlake_On;
@@ -246,8 +255,19 @@
    Has_FDI_RX_Power_Down         : <genbool> := Gen_Haswell;
 
    ---------- Clocks: -----------
-   Has_GMCH_RawClk               : <genbool> := Gen_G45;
+   Has_GMCH_RawClk               : <genbool> := Up_To_G45;
    Has_GMCH_Mobile_VCO           : <g45bool> := GMCH_GM45;
+
+   ---------- I945-specific: ----
+   Has_I945_GTT_BAR              : <genbool> := Gen_I945;
+   Has_I945_Simple_GTT_PTE       : <genbool> := Gen_I945;
+   Has_Gen3_Fences               : <genbool> := Gen_I945;
+   -- Pre-i965: LVDS encoder can only source from Pipe B (Secondary)
+   LVDS_Needs_Pipe_B             : <genbool> := Gen_I945;
+   -- Pre-Gen5: DSPCNTR has a pipe select field (bits 25:24)
+   Has_DSPCNTR_Pipe_Select       : <genbool> := Up_To_G45;
+   -- Gen3: Plane A feeds Pipe B, Plane B feeds Pipe A (for FBC + LVDS)
+   Planes_Pipes_Swapped          : <genbool> := Gen_I945;
    Has_Broadwell_CDClk           : <hswbool> := CPU_Broadwell;
    Can_Switch_CDClk              : <hswbool> := Broadwell_On;
    Has_Fractional_RawClk         : <genbool> := Cannon_Point_On;
@@ -283,6 +303,7 @@
    Ungate_GMBUS_Unit_Level       : <genbool> := Skylake_On and not Tigerlake_On;
    GMBUS_Alternative_Pins        : <genbool> := Gen_Broxton or Cannon_Point_On;
    Has_PCH_GMBUS                 : <genbool> := Ironlake_On;
+   Has_GMCH_GMBUS                : <genbool> := Up_To_G45;
 
    ----------- Power: -----------
    Has_IPS                       : <hswbool> :=
@@ -344,8 +365,9 @@
 
    ----------------------------------------------------------------------------
 
-   Default_CDClk_Freq : <ilkhswvar> CDClk_Range :=
-     (if    Gen_G45                          then 320_000_000  -- unused
+   Default_CDClk_Freq : <i945ilkhswvar> CDClk_Range :=
+     (if    Gen_I945                         then 200_000_000  -- unused, depends on GCFGC
+      elsif Gen_G45                          then 320_000_000  -- unused
       elsif CPU_Ironlake                     then 450_000_000
       elsif CPU_Sandybridge or CPU_Ivybridge then 400_000_000
       elsif Gen_Haswell and then Is_ULX      then 337_500_000
@@ -356,7 +378,8 @@
                                              else CDClk_Range'First);
 
    Default_RawClk_Freq : <hswvar> Frequency_Type :=
-     (if    Gen_G45        then 100_000_000  -- unused, depends on FSB
+     (if    Gen_I945       then 100_000_000  -- unused, depends on FSB
+      elsif Gen_G45        then 100_000_000  -- unused, depends on FSB
       elsif Gen_Ironlake   then 125_000_000
       elsif Gen_Haswell    then (if Is_LP then 24_000_000 else 125_000_000)
       elsif Gen_Broxton    then Frequency_Type'First  -- none needed
@@ -372,7 +395,7 @@
    type Width_Per_Pipe is array (Pipe_Index) of Width_Type;
 
    Maximum_Scalable_Width : <hswvar> Width_Per_Pipe :=
-     (if Gen_G45 then   -- TODO: Is this true?
+     (if Gen_I945 or Gen_G45 then
         (Primary     => 4096,
          Secondary   => 2048,
          Tertiary    => Pos32'First)
@@ -388,7 +411,7 @@
    -- Maximum X position of hardware cursors
    Maximum_Cursor_X : constant :=
      (case Gen is
-         when G45 .. Ironlake       => 4095,
+         when I945 .. Ironlake      => 4095,
          when Haswell .. Tigerlake  => 8191);
 
    Maximum_Cursor_Y : constant := 4095;
@@ -398,7 +421,7 @@
    -- FIXME: Unknown for Broxton, Linux' i915 contains a fixme too :-D
    HDMI_Max_Clock_24bpp : constant Frequency_Type :=
      (case Gen is
-         when Generation'First .. G45      => 165_000_000,
+         when Generation'First .. G45      => 165_000_000, -- i945: no HDMI, moot
          when Ironlake                     => 225_000_000,
          when Haswell .. Skylake           => 300_000_000,
 	 when Tigerlake .. Generation'Last => 600_000_000);
@@ -412,8 +435,10 @@
 
    GTT_PTE_Size : <hswvar> Natural := (if Has_64bit_GTT then 8 else 4);
 
-   Fence_Base : <ilkvar> Natural :=
-     (if not Sandybridge_On then 16#0000_3000# else 16#0010_0000#);
+   Fence_Base : <i945ilkvar> Natural :=
+     (if    Gen_I945         then 16#0000_2000#
+      elsif not Sandybridge_On then 16#0000_3000#
+      else                          16#0010_0000#);
 
    Fence_Count : <ilkvar> Natural :=
      (if not Ivybridge_On then 16 else 32);
@@ -560,6 +585,8 @@
    function Is_GPU (Device_Id : Word16; CPU : CPU_Type; CPU_Var : CPU_Variant)
       return Boolean is
      (case CPU is
+         when I945G        => Device_Id = 16#2772#,
+         when I945GM       => Device_Id = 16#27a2# or Device_Id = 16#27ae#,
          when G45          => (Device_Id and 16#ff02#) = 16#2e02#,
          when GM45         => (Device_Id and 16#fffe#) = 16#2a42#,
          when Ironlake     => (Device_Id and 16#fff3#) = 16#0042#,
diff --git a/common/hw-gfx-gma-config_helpers.adb b/common/hw-gfx-gma-config_helpers.adb
index 868d514..3b55968 100644
--- a/common/hw-gfx-gma-config_helpers.adb
+++ b/common/hw-gfx-gma-config_helpers.adb
@@ -29,15 +29,22 @@
    begin
       return
         (case Config.Gen is
+            when I945 =>               -- LVDS, VGA, SDVO B/C on GMCH
+              (case Port is
+                  when LVDS         => LVDS,
+                  when HDMI1 | DP1  => DIGI_B, -- SDVO B
+                  when HDMI2 | DP2  => DIGI_C, -- SDVO C
+                  when Analog       => VGA,
+                  when others       => DIGI_A), -- n/a, actually
             when G45 =>                -- everything on GMCH
-               (case Port is
-                   when LVDS         => LVDS,
-                   when eDP          => DIGI_A, -- n/a, actually
-                   when HDMI1 | DP1  => DIGI_B,
-                   when HDMI2 | DP2  => DIGI_C,
-                   when HDMI3 | DP3  => DIGI_D,
-                   when Analog       => VGA,
-                   when others       => DIGI_A), -- n/a, actually
+              (case Port is
+                  when LVDS         => LVDS,
+                  when eDP          => DIGI_A, -- n/a, actually
+                  when HDMI1 | DP1  => DIGI_B,
+                  when HDMI2 | DP2  => DIGI_C,
+                  when HDMI3 | DP3  => DIGI_D,
+                  when Analog       => VGA,
+                  when others       => DIGI_A), -- n/a, actually
             when Ironlake =>           -- everything but eDP through FDI/PCH
               (if Port = eDP then
                   DIGI_A
diff --git a/common/hw-gfx-gma-display_probing.adb b/common/hw-gfx-gma-display_probing.adb
index 67f8ddf..b313ded 100644
--- a/common/hw-gfx-gma-display_probing.adb
+++ b/common/hw-gfx-gma-display_probing.adb
@@ -209,6 +209,19 @@
          exit when not Success;
       end loop;
 
+      -- On pre-i965 hardware, LVDS must use Pipe B (Secondary).
+      -- If the scan assigned LVDS to Primary, swap with Secondary.
+      if Config.LVDS_Needs_Pipe_B and
+         Configs (Primary).Port = LVDS
+      then
+         declare
+            Tmp : constant Pipe_Config := Configs (Secondary);
+         begin
+            Configs (Secondary) := Configs (Primary);
+            Configs (Primary) := Tmp;
+         end;
+      end if;
+
       -- Restore power settings
       if not Keep_Power then
          Power_And_Clocks.Power_Set_To (Cur_Configs);
diff --git a/common/hw-gfx-gma-pipe_setup.adb b/common/hw-gfx-gma-pipe_setup.adb
index 82f5222..03aaf12 100644
--- a/common/hw-gfx-gma-pipe_setup.adb
+++ b/common/hw-gfx-gma-pipe_setup.adb
@@ -27,6 +27,8 @@
    DSPCNTR_ENABLE                      : constant :=  1 * 2 ** 31;
    DSPCNTR_GAMMA_CORRECTION            : constant :=  1 * 2 ** 30;
    DSPCNTR_FORMAT_MASK                 : constant := 15 * 2 ** 26;
+   DSPCNTR_PIPE_SEL_MASK               : constant :=  3 * 2 ** 24;
+   DSPCNTR_PIPE_B_SELECT               : constant :=  1 * 2 ** 24;
    DSPCNTR_DISABLE_TRICKLE_FEED        : constant :=  1 * 2 ** 14;
    DSPCNTR_TILED_SURFACE_LINEAR        : constant :=  0 * 2 ** 10;
    DSPCNTR_TILED_SURFACE_X_TILED       : constant :=  1 * 2 ** 10;
@@ -36,10 +38,14 @@
       X_Tiled  => DSPCNTR_TILED_SURFACE_X_TILED,
       Y_Tiled  => 0); -- unsupported
 
+   function DSPCNTR_PIPE_SEL (Pipe : Pipe_Index) return Word32 is
+     (if Pipe = Secondary then DSPCNTR_PIPE_B_SELECT else 0);
+
    DSPCNTR_MASK : constant Word32 :=
       DSPCNTR_ENABLE or
       DSPCNTR_GAMMA_CORRECTION or
       DSPCNTR_FORMAT_MASK or
+      DSPCNTR_PIPE_SEL_MASK or
       DSPCNTR_DISABLE_TRICKLE_FEED or
       DSPCNTR_TILED_SURFACE_X_TILED;
 
@@ -212,7 +218,7 @@
    is
       -- FIXME: setup correct format, based on framebuffer RGB format
       Format : constant Word32 := 6 * 2 ** 26;
-      PRI : Word32 := DSPCNTR_ENABLE or Format;
+      PRI : Word32 := Format;
    begin
       pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
 
@@ -245,11 +251,19 @@
             Registers.Write (Controller.PLANE_SURF, FB.Offset and 16#ffff_f000#);
          end;
       else
+         if Config.Has_DSPCNTR_Pipe_Select then
+            PRI := PRI or DSPCNTR_PIPE_SEL (Controller.Pipe);
+         end if;
          if Config.Disable_Trickle_Feed then
             PRI := PRI or DSPCNTR_DISABLE_TRICKLE_FEED;
          end if;
-         -- for now, just disable gamma LUT (can't do anything
-         -- useful without colorimetry information from display)
+
+         -- Write DSPCNTR *without* the enable bit first.  On pre-SKL
+         -- hardware the control register self-arms when the plane
+         -- transitions from disabled to enabled, latching whatever
+         -- stride/size/offset values happen to be in the registers at
+         -- that moment.  Programming format, pipe-select, and trickle-
+         -- feed now avoids a glitch with stale geometry values.
          Registers.Unset_And_Set_Mask
             (Register   => Controller.DSPCNTR,
              Mask_Unset => DSPCNTR_MASK,
@@ -257,6 +271,16 @@
 
          Registers.Write
            (Controller.DSPSTRIDE, Word32 (Pixel_To_Bytes (FB.Stride, FB)));
+
+         -- Gen3 (i945): program DSPSIZE and DSPPOS before the surface
+         -- address write that arms the double-buffered plane registers.
+         if Config.Gen_I945 then
+            Registers.Write
+              (Controller.DSPSIZE,
+               Encode (Rotated_Width (FB), Rotated_Height (FB)));
+            Registers.Write (Controller.DSPPOS, 16#0000_0000#);
+         end if;
+
          if Config.Has_DSP_Linoff and then FB.Tiling = Linear then
             pragma Assert_And_Cut (True);
             declare
@@ -265,19 +289,33 @@
             begin
                Registers.Write
                  (Register => Controller.DSPLINOFF,
-                  Value    => Word32 (Pixel_To_Bytes (Linear_Offset, FB)));
+                  Value    => (if Config.Has_DSPSURF
+                               then Word32 (Pixel_To_Bytes (Linear_Offset, FB))
+                               else (FB.Offset and 16#ffff_f000#) or
+                                    Word32 (Pixel_To_Bytes (Linear_Offset, FB))));
                Registers.Write (Controller.DSPTILEOFF, 0);
             end;
          else
             if Config.Has_DSP_Linoff then
-               Registers.Write (Controller.DSPLINOFF, 0);
+               Registers.Write (Controller.DSPLINOFF,
+                  (if Config.Has_DSPSURF then 0
+                   else FB.Offset and 16#ffff_f000#));
             end if;
             Registers.Write
               (Register => Controller.DSPTILEOFF,
                Value    => Shift_Left (Word32 (FB.Start_Y), 16) or
                            Word32 (FB.Start_X));
          end if;
-         Registers.Write (Controller.DSPSURF, FB.Offset and 16#ffff_f000#);
+         if Config.Has_DSPSURF then
+            Registers.Write (Controller.DSPSURF, FB.Offset and 16#ffff_f000#);
+         end if;
+
+         -- Now enable the plane.  All geometry registers are in place,
+         -- so the self-arm latches correct values.
+         Registers.Write
+           (Register => Controller.DSPCNTR,
+            Value    => DSPCNTR_ENABLE or PRI or
+                        DSPCNTR_TILED_SURFACE (FB.Tiling));
       end if;
    end Setup_Hires_Plane;
 
@@ -579,11 +617,17 @@
    is
       Used_For_Secondary : Boolean;
    begin
-      Registers.Is_Set_Mask
-        (Register => Registers.GMCH_PFIT_CONTROL,
-         Mask     => GMCH_PFIT_CONTROL_SELECT_PIPE_B,
-         Result   => Used_For_Secondary);
-      Pipe := (if Used_For_Secondary then Secondary else Primary);
+      if Config.Gen_I945 then
+         -- Gen3: panel fitter is hardwired to Pipe B (Secondary).
+         -- The PFIT_PIPE field (bits 30:29) does not exist on Gen3.
+         Pipe := Secondary;
+      else
+         Registers.Is_Set_Mask
+           (Register => Registers.GMCH_PFIT_CONTROL,
+            Mask     => GMCH_PFIT_CONTROL_SELECT_PIPE_B,
+            Result   => Used_For_Secondary);
+         Pipe := (if Used_For_Secondary then Secondary else Primary);
+      end if;
    end;
 
    procedure Panel_Fitter_Off (Controller : Controller_Type)
@@ -604,7 +648,10 @@
       elsif Config.Has_GMCH_PFIT_CONTROL then
          Gmch_Panel_Fitter_Pipe (Pipe_Using_PF);
          if Pipe_Using_PF = Controller.Pipe then
-            Registers.Unset_Mask (Registers.GMCH_PFIT_CONTROL, PF_CTRL_ENABLE);
+            -- Write 0 to clear all bits (enable, scaling mode, auto-scale,
+            -- interpolation).  Just clearing the enable bit can leave stale
+            -- Gen3 auto-scale bits that confuse the hardware.
+            Registers.Write (Registers.GMCH_PFIT_CONTROL, 16#0000_0000#);
          end if;
       else
          Registers.Unset_Mask (Controller.PF_CTRL, PF_CTRL_ENABLE);
@@ -758,7 +805,7 @@
 
       Legacy_VGA_Off;
 
-      for Pipe in Pipe_Index loop
+      for Pipe in Pipe_Index range Pipe_Index'First .. Config.Max_Pipe loop
          Planes_Off (Controllers (Pipe), Cursors (Pipe));
          Transcoder.Off (Pipe);
          Panel_Fitter_Off (Controllers (Pipe));
diff --git a/common/hw-gfx-gma-pipe_setup.ads b/common/hw-gfx-gma-pipe_setup.ads
index 7270cb2..f047a64 100644
--- a/common/hw-gfx-gma-pipe_setup.ads
+++ b/common/hw-gfx-gma-pipe_setup.ads
@@ -82,6 +82,8 @@
          DSPSTRIDE         : Registers.Registers_Index;
          DSPSURF           : Registers.Registers_Index;
          DSPTILEOFF        : Registers.Registers_Index;
+         DSPSIZE           : Registers.Registers_Index;
+         DSPPOS            : Registers.Registers_Index;
          SPCNTR            : Registers.Registers_Index;
          -- Skylake registers (partially aliased)
          PLANE_CTL         : Registers.Registers_Index;
@@ -104,6 +106,11 @@
 
    type Controller_Array is array (Pipe_Index) of Controller_Type;
 
+   --  On Gen3 (i945), FBC only works with Plane A, but LVDS and the
+   --  panel fitter are hooked to Pipe B. Hence we want Plane A feeding
+   --  Pipe B, which means swapping the plane register assignments:
+   --  Primary (Pipe A) gets Plane B, Secondary (Pipe B) gets Plane A.
+
    Controllers : constant Controller_Array :=
      (Primary => Controller_Type'
         (Pipe              => Primary,
@@ -112,11 +119,27 @@
          PF_CTRL           => Registers.PFA_CTL_1,
          PF_WIN_POS        => Registers.PFA_WIN_POS,
          PF_WIN_SZ         => Registers.PFA_WIN_SZ,
-         DSPCNTR           => Registers.DSPACNTR,
-         DSPLINOFF         => Registers.DSPALINOFF,
-         DSPSTRIDE         => Registers.DSPASTRIDE,
-         DSPSURF           => Registers.DSPASURF,
-         DSPTILEOFF        => Registers.DSPATILEOFF,
+         DSPCNTR           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPBCNTR else Registers.DSPACNTR),
+         DSPLINOFF         =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPBLINOFF else Registers.DSPALINOFF),
+         DSPSTRIDE         =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPBSTRIDE else Registers.DSPASTRIDE),
+         DSPSURF           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPBSURF else Registers.DSPASURF),
+         DSPTILEOFF        =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPBTILEOFF else Registers.DSPATILEOFF),
+         DSPSIZE           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.PLANE_SIZE_1_B else Registers.PLANE_SIZE_1_A),
+         DSPPOS            =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.PLANE_POS_1_B else Registers.PLANE_POS_1_A),
          SPCNTR            => Registers.SPACNTR,
          PLANE_CTL         => Registers.DSPACNTR,
          PLANE_OFFSET      => Registers.DSPATILEOFF,
@@ -157,11 +180,27 @@
          PF_CTRL           => Registers.PFB_CTL_1,
          PF_WIN_POS        => Registers.PFB_WIN_POS,
          PF_WIN_SZ         => Registers.PFB_WIN_SZ,
-         DSPCNTR           => Registers.DSPBCNTR,
-         DSPLINOFF         => Registers.DSPBLINOFF,
-         DSPSTRIDE         => Registers.DSPBSTRIDE,
-         DSPSURF           => Registers.DSPBSURF,
-         DSPTILEOFF        => Registers.DSPBTILEOFF,
+         DSPCNTR           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPACNTR else Registers.DSPBCNTR),
+         DSPLINOFF         =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPALINOFF else Registers.DSPBLINOFF),
+         DSPSTRIDE         =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPASTRIDE else Registers.DSPBSTRIDE),
+         DSPSURF           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPASURF else Registers.DSPBSURF),
+         DSPTILEOFF        =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.DSPATILEOFF else Registers.DSPBTILEOFF),
+         DSPSIZE           =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.PLANE_SIZE_1_A else Registers.PLANE_SIZE_1_B),
+         DSPPOS            =>
+           (if Config.Planes_Pipes_Swapped
+            then Registers.PLANE_POS_1_A else Registers.PLANE_POS_1_B),
          SPCNTR            => Registers.SPBCNTR,
          PLANE_CTL         => Registers.DSPBCNTR,
          PLANE_OFFSET      => Registers.DSPBTILEOFF,
@@ -207,6 +246,8 @@
          DSPSTRIDE         => Registers.DSPCSTRIDE,
          DSPSURF           => Registers.DSPCSURF,
          DSPTILEOFF        => Registers.DSPCTILEOFF,
+         DSPSIZE           => Registers.PLANE_SIZE_1_C,
+         DSPPOS            => Registers.PLANE_POS_1_C,
          SPCNTR            => Registers.SPCCNTR,
          PLANE_CTL         => Registers.DSPCCNTR,
          PLANE_OFFSET      => Registers.DSPCTILEOFF,
diff --git a/common/hw-gfx-gma-registers.adb b/common/hw-gfx-gma-registers.adb
index 3f0d7ae..eb12b0d 100644
--- a/common/hw-gfx-gma-registers.adb
+++ b/common/hw-gfx-gma-registers.adb
@@ -75,20 +75,57 @@
 
    FENCE_PAGE_SHIFT                    : constant := 12;
    FENCE_PAGE_MASK                     : constant := 16#ffff_f000#;
-   FENCE_TILE_WALK_YMAJOR              : constant := 1 * 2 ** 1;
    FENCE_VALID                         : constant := 1 * 2 ** 0;
 
+   -- Gen4+ (i965/G45+): 64-bit fence pairs at Fence_Base + i*8
+   FENCE_TILE_WALK_YMAJOR              : constant := 1 * 2 ** 1;
+
    function Fence_Lower_Idx (Fence : Fence_Range) return Registers_Range is
       (Registers_Range (Config.Fence_Base / Register_Width + 2 * Fence));
    function Fence_Upper_Idx (Fence : Fence_Range) return Registers_Range is
       (Fence_Lower_Idx (Fence) + 1);
 
+   -- Gen3 (i915/i945): 32-bit fences, split layout:
+   --   Fences 0-7:  0x2000 + i*4
+   --   Fences 8-15: 0x3000 + (i-8)*4
+   -- FENCE_REG(i) = 0x2000 + (((i) & 8) << 9) + ((i) & 7) * 4
+   GEN3_FENCE_TILING_Y_SHIFT           : constant := 12;
+   GEN3_FENCE_SIZE_SHIFT               : constant := 8;
+   GEN3_FENCE_PITCH_SHIFT              : constant := 4;
+
+   function Gen3_Fence_Idx (Fence : Fence_Range) return Registers_Range is
+      (Registers_Range
+         ((16#2000# + (Fence / 8) * 16#1000# + (Fence mod 8) * 4) /
+          Register_Width));
+
+   -- Compute floor(log2(n)) for n >= 1 (fence size/pitch encoding).
+   function Floor_Log2 (N : Word32) return Natural
+   with
+      Pre => N >= 1
+   is
+      Result : Natural := 0;
+      Val    : Word32 := N;
+   begin
+      for I in 0 .. 31 loop
+         exit when Val <= 1;
+         Val    := Shift_Right (Val, 1);
+         Result := I + 1;
+      end loop;
+      return Result;
+   end Floor_Log2;
+
    procedure Clear_Fences
    is
    begin
-      for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
-         Regs.Write (Fence_Lower_Idx (Fence), 0);
-      end loop;
+      if Config.Has_Gen3_Fences then
+         for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+            Regs.Write (Gen3_Fence_Idx (Fence), 0);
+         end loop;
+      else
+         for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+            Regs.Write (Fence_Lower_Idx (Fence), 0);
+         end loop;
+      end if;
    end Clear_Fences;
 
    procedure Add_Fence
@@ -111,42 +148,106 @@
       pragma Debug (Debug.Put_Line (" tiles per row."));
 
       Success := False;
-      for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
-         Regs.Read (Reg32, Fence_Lower_Idx (Fence));
-         if (Reg32 and FENCE_VALID) = 0 then
-            Regs.Write
-              (Index => Fence_Lower_Idx (Fence),
-               Value => Shift_Left (Word32 (First_Page), FENCE_PAGE_SHIFT) or
-                        (if Y_Tiles then FENCE_TILE_WALK_YMAJOR else 0) or
-                        FENCE_VALID);
-            Regs.Write
-              (Index => Fence_Upper_Idx (Fence),
-               Value => Shift_Left (Word32 (Last_Page), FENCE_PAGE_SHIFT) or
-                        Word32 (Pitch) * (if Y_Tiles then 1 else 4) - 1);
-            Success := True;
-            exit;
-         end if;
-      end loop;
+
+      if Config.Has_Gen3_Fences then
+         -- Gen3 i945: single 32-bit fence register per fence
+         -- Format: start[31:20] | tiling_y[12] | size_bits[11:8] |
+         --         pitch_log2[7:4] | valid[0]
+         -- stride: Y-tiled /128, X-tiled /512 (i945 has 128-byte Y tiling)
+         -- size_bits: log2(size_in_pages / 256) = log2(size_in_MB)
+         for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+            Regs.Read (Reg32, Gen3_Fence_Idx (Fence));
+            if (Reg32 and FENCE_VALID) = 0 then
+               declare
+                  Start_Addr : constant Word32 :=
+                     Shift_Left (Word32 (First_Page), FENCE_PAGE_SHIFT);
+                  Size_Pages : constant Word32 :=
+                     Word32 (Last_Page - First_Page + 1);
+                  -- Size in MB (pages / 256, since page = 4KB, 256*4KB = 1MB)
+                  Size_MB    : constant Word32 := Size_Pages / 256;
+                  -- Pitch in tiles (X: 512B tiles, Y: 128B tiles for i945)
+                  Stride     : constant Word32 :=
+                     Word32 (Pitch) / (if Y_Tiles then 128 else 512);
+                  Size_Bits  : constant Word32 :=
+                     (if Size_MB >= 1
+                      then Word32 (Floor_Log2 (Size_MB))
+                      else 0);
+               begin
+                  Regs.Write
+                    (Index => Gen3_Fence_Idx (Fence),
+                     Value => Start_Addr or
+                              (if Y_Tiles
+                               then Shift_Left (1, GEN3_FENCE_TILING_Y_SHIFT)
+                               else 0) or
+                              Shift_Left (Size_Bits, GEN3_FENCE_SIZE_SHIFT) or
+                              Shift_Left
+                                ((if Stride >= 1
+                                  then Word32 (Floor_Log2 (Stride))
+                                  else 0),
+                                 GEN3_FENCE_PITCH_SHIFT) or
+                              FENCE_VALID);
+               end;
+               Success := True;
+               exit;
+            end if;
+         end loop;
+      else
+         -- Gen4+ (i965/G45+): 64-bit fence register pairs
+         for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+            Regs.Read (Reg32, Fence_Lower_Idx (Fence));
+            if (Reg32 and FENCE_VALID) = 0 then
+               Regs.Write
+                 (Index => Fence_Lower_Idx (Fence),
+                  Value => Shift_Left (Word32 (First_Page), FENCE_PAGE_SHIFT) or
+                           (if Y_Tiles then FENCE_TILE_WALK_YMAJOR else 0) or
+                           FENCE_VALID);
+               Regs.Write
+                 (Index => Fence_Upper_Idx (Fence),
+                  Value => Shift_Left (Word32 (Last_Page), FENCE_PAGE_SHIFT) or
+                           Word32 (Pitch) * (if Y_Tiles then 1 else 4) - 1);
+               Success := True;
+               exit;
+            end if;
+         end loop;
+      end if;
    end Add_Fence;
 
    procedure Remove_Fence (First_Page, Last_Page : GTT_Range)
    is
       Page_Lower : constant Word32 :=
          Shift_Left (Word32 (First_Page), FENCE_PAGE_SHIFT);
-      Page_Upper : constant Word32 :=
-         Shift_Left (Word32 (Last_Page), FENCE_PAGE_SHIFT);
-      Fence_Upper, Fence_Lower : Word32;
+      Reg32 : Word32;
    begin
-      for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
-         Regs.Read (Fence_Lower, Fence_Lower_Idx (Fence));
-         Regs.Read (Fence_Upper, Fence_Upper_Idx (Fence));
-         if (Fence_Lower and FENCE_PAGE_MASK) = Page_Lower and
-            (Fence_Upper and FENCE_PAGE_MASK) = Page_Upper
-         then
-            Regs.Write (Fence_Lower_Idx (Fence), 0);
-            exit;
-         end if;
-      end loop;
+      if Config.Has_Gen3_Fences then
+         -- Gen3: match start address in single 32-bit register
+         for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+            Regs.Read (Reg32, Gen3_Fence_Idx (Fence));
+            if (Reg32 and FENCE_VALID) /= 0 and
+               (Reg32 and 16#fff0_0000#) = (Page_Lower and 16#fff0_0000#)
+            then
+               Regs.Write (Gen3_Fence_Idx (Fence), 0);
+               exit;
+            end if;
+         end loop;
+      else
+         -- Gen4+: match start in lower, end in upper register
+         declare
+            Page_Upper : constant Word32 :=
+               Shift_Left (Word32 (Last_Page), FENCE_PAGE_SHIFT);
+            Fence_Upper, Fence_Lower : Word32;
+         begin
+            for Fence in Fence_Range range 0 .. Config.Fence_Count - 1 loop
+               Regs.Read (Fence_Lower, Fence_Lower_Idx (Fence));
+               Regs.Read (Fence_Upper, Fence_Upper_Idx (Fence));
+               if (Fence_Lower and FENCE_PAGE_MASK) = Page_Lower and
+                  (Fence_Upper and FENCE_PAGE_MASK) = Page_Upper
+               then
+                  Regs.Write (Fence_Lower_Idx (Fence), 0);
+                  exit;
+               end if;
+            end loop;
+         end;
+      end if;
    end Remove_Fence;
 
    ----------------------------------------------------------------------------
@@ -157,7 +258,13 @@
       Valid          : Boolean)
    is
    begin
-      if not Config.Has_64bit_GTT then
+      if Config.Has_I945_Simple_GTT_PTE then
+         -- i945: simple 32-bit PTE, no high address bits
+         GTT_32.Write
+           (Index => GTT_Page,
+            Value => GTT_PTE_32 (Device_Address and 16#ffff_f000#) or
+                     Boolean'Pos (Valid));
+      elsif not Config.Has_64bit_GTT then
          GTT_32.Write
            (Index => GTT_Page,
             Value => GTT_PTE_32 (Device_Address and 16#ffff_f000#) or
@@ -178,7 +285,15 @@
       GTT_Page       : in     GTT_Range)
    is
    begin
-      if not Config.Has_64bit_GTT then
+      if Config.Has_I945_Simple_GTT_PTE then
+         declare
+            PTE : GTT_PTE_32;
+         begin
+            GTT_32.Read (PTE, GTT_Page);
+            Valid := (PTE and GTT_PTE_Valid) /= 0;
+            Device_Address := GTT_Address_Type (PTE and 16#ffff_f000#);
+         end;
+      elsif not Config.Has_64bit_GTT then
          declare
             PTE : GTT_PTE_32;
          begin
diff --git a/common/hw-gfx-gma-registers.ads b/common/hw-gfx-gma-registers.ads
index 498cb53..404dda3 100644
--- a/common/hw-gfx-gma-registers.ads
+++ b/common/hw-gfx-gma-registers.ads
@@ -2395,7 +2395,9 @@
       Last_Page   : in     GTT_Range;
       Tiling      : in     XY_Tiling;
       Pitch       : in     Natural;
-      Success     :    out Boolean);
+      Success     :    out Boolean)
+   with
+      Pre => Last_Page >= First_Page;
 
    procedure Remove_Fence (First_Page, Last_Page : GTT_Range);
 
diff --git a/common/hw-gfx-gma.adb b/common/hw-gfx-gma.adb
index c3073d3..92c6f5e 100644
--- a/common/hw-gfx-gma.adb
+++ b/common/hw-gfx-gma.adb
@@ -496,7 +496,9 @@
       use type HW.Word64;
 
       function MMIO_GTT_Offset return Natural is
-        (if Config.Has_64bit_GTT
+        (if Config.Has_I945_GTT_BAR
+         then 0  -- i945: GTT is on separate BAR3, not within BAR0
+         elsif Config.Has_64bit_GTT
          then Registers.MMIO_GTT_64_Offset
          else Registers.MMIO_GTT_32_Offset);
       PCI_MMIO_Base, PCI_GTT_Base : Word64;
@@ -507,7 +509,14 @@
       is
          Audio_VID_DID : Word32;
       begin
+         if Config.Gen_I945 then
+            -- i945 has no integrated audio DID to verify
+            Success := True;
+            return;
+         end if;
          case Config.Gen is
+            when I945 =>
+               Audio_VID_DID := 0;  -- unreachable due to early return
             when G45 =>
                Registers.Read (Registers.G4X_AUD_VID_DID, Audio_VID_DID);
             when Ironlake =>
@@ -568,14 +577,20 @@
       if Success then
          Check_Platform_PCI (Success);
          if Success then
-            Dev.Map (PCI_MMIO_Base, PCI.Res0, Length => MMIO_GTT_Offset);
-            Dev.Map (PCI_GTT_Base, PCI.Res0, Offset => MMIO_GTT_Offset);
+            if Config.Has_I945_GTT_BAR then
+               -- i945: MMIO is on BAR0, GTT is on separate BAR3
+               Dev.Map (PCI_MMIO_Base, PCI.Res0);
+               Dev.Map (PCI_GTT_Base, PCI.Res3);
+            else
+               Dev.Map (PCI_MMIO_Base, PCI.Res0, Length => MMIO_GTT_Offset);
+               Dev.Map (PCI_GTT_Base, PCI.Res0, Offset => MMIO_GTT_Offset);
+            end if;
             if PCI_MMIO_Base /= 0 and PCI_GTT_Base /= 0 then
                Registers.Set_Register_Base (PCI_MMIO_Base, PCI_GTT_Base);
                PCI_Usable := True;
             else
                pragma Debug (Debug.Put_Line
-                 ("ERROR: Couldn't map resoure0."));
+                 ("ERROR: Couldn't map resource0."));
                Success := Config.Default_MMIO_Base_Set;
             end if;
          end if;
@@ -828,11 +843,17 @@
       Pre => Is_Initialized
    is
       GGC_Reg : constant PCI.Index :=
-        (if Config.Gen_G45 or Config.CPU_Ironlake then 16#52# else 16#50#);
+        (if Config.Gen_I945 or Config.Gen_G45 or Config.CPU_Ironlake
+         then 16#52# else 16#50#);
       GGC : Word16;
    begin
       Dev.Read16 (GGC, GGC_Reg);
-      if Config.Gen_G45 or Config.CPU_Ironlake then
+      if Config.Gen_I945 then
+         -- i945 GTT is on a separate BAR3; GGC GGMS encoding differs
+         -- from Gen4+.  Match the Linux driver and use the BAR size.
+         Dev.Resource_Size (GTT_Size, PCI.Res3);
+         Stolen_Size := Stolen_Size_Gen4 (GGC);
+      elsif Config.Gen_G45 or Config.CPU_Ironlake then
          GTT_Size    := GTT_Size_Gen4 (GGC);
          Stolen_Size := Stolen_Size_Gen4 (GGC);
       elsif Config.CPU_Sandybridge or Config.CPU_Ivybridge or Config.CPU_Haswell
@@ -848,6 +869,46 @@
       end if;
    end Decode_Stolen;
 
+   procedure GTT_Entry_Count (Count : out Natural)
+   is
+      GTT_Size : Natural;
+
+      procedure Fake_Config_State_Access
+      with
+         Global => (Input => Config.Variable),
+         Annotate => (GNATprove, Intentional, "unused global",
+            "Used to have a common contract across platforms.");
+      procedure Fake_Config_State_Access is null;
+   begin
+      Fake_Config_State_Access;
+
+      if Config.Has_I945_GTT_BAR then
+         -- i945 GTT is on a separate BAR3; its size is the BAR size.
+         Dev.Resource_Size (GTT_Size, PCI.Res3);
+      else
+         -- Gen4+: GTT size is encoded in the GGC register.
+         declare
+            GGC_Reg : constant PCI.Index :=
+              (if Config.Gen_G45 or Config.CPU_Ironlake
+               then 16#52# else 16#50#);
+            GGC : Word16;
+         begin
+            Dev.Read16 (GGC, GGC_Reg);
+            if Config.Gen_G45 or Config.CPU_Ironlake then
+               GTT_Size := GTT_Size_Gen4 (GGC);
+            elsif Config.CPU_Sandybridge or
+                  Config.CPU_Ivybridge or
+                  Config.CPU_Haswell
+            then
+               GTT_Size := GTT_Size_Gen6 (GGC);
+            else
+               GTT_Size := GTT_Size_Gen8 (GGC);
+            end if;
+         end;
+      end if;
+      Count := Natural'Min (GTT_Size / Config.GTT_PTE_Size, GTT_Range'Last + 1);
+   end GTT_Entry_Count;
+
    -- Additional runtime validation that FB fits stolen memory and aperture.
    procedure Validate_FB (FB : Framebuffer_Type; Valid : out Boolean)
    with
diff --git a/common/hw-gfx-gma.ads b/common/hw-gfx-gma.ads
index 43e88e1..c05f4f1 100644
--- a/common/hw-gfx-gma.ads
+++ b/common/hw-gfx-gma.ads
@@ -35,10 +35,12 @@
    subtype GTT_Range is Natural range 0 .. 16#8_0000# - 1;
    GTT_Rotation_Offset : constant GTT_Range := GTT_Range'Last / 2 + 1;
 
-   type Generation is (G45, Ironlake, Haswell, Broxton, Skylake, Tigerlake);
+   type Generation is (I945, G45, Ironlake, Haswell, Broxton, Skylake, Tigerlake);
 
    type CPU_Type is
-     (G45,
+     (I945G,
+      I945GM,
+      G45,
       GM45,
       Ironlake,
       Sandybridge,
@@ -227,6 +229,13 @@
          In_Out => Device_State, Proof_In => Init_State),
       Pre => Is_Initialized;
 
+   procedure GTT_Entry_Count (Count : out Natural)
+   with
+      Global =>
+        (Input => State,
+         In_Out => Device_State, Proof_In => Init_State),
+      Pre => Is_Initialized and HW.Config.Dynamic_MMIO;
+
    procedure Setup_Default_FB
      (FB       : in     Framebuffer_Type;
       Clear    : in     Boolean := True;
diff --git a/common/i945/Makefile.inc b/common/i945/Makefile.inc
new file mode 100644
index 0000000..3c7d022
--- /dev/null
+++ b/common/i945/Makefile.inc
@@ -0,0 +1,11 @@
+gfxinit-y += hw-gfx-gma-connectors.adb
+gfxinit-y += hw-gfx-gma-gmch-lvds.adb
+gfxinit-y += hw-gfx-gma-gmch-lvds.ads
+gfxinit-y += hw-gfx-gma-gmch-vga.adb
+gfxinit-y += hw-gfx-gma-gmch-vga.ads
+gfxinit-y += hw-gfx-gma-plls.adb
+gfxinit-y += hw-gfx-gma-plls.ads
+gfxinit-y += hw-gfx-gma-port_detect.adb
+gfxinit-y += hw-gfx-gma-gmch.ads
+gfxinit-y += hw-gfx-gma-power_and_clocks.ads
+gfxinit-y += hw-gfx-gma-power_and_clocks.adb
diff --git a/common/i945/hw-gfx-gma-connectors.adb b/common/i945/hw-gfx-gma-connectors.adb
new file mode 100644
index 0000000..e774227
--- /dev/null
+++ b/common/i945/hw-gfx-gma-connectors.adb
@@ -0,0 +1,104 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Panel;
+with HW.GFX.GMA.GMCH.VGA;
+with HW.GFX.GMA.GMCH.LVDS;
+
+with HW.Debug;
+with GNAT.Source_Info;
+
+package body HW.GFX.GMA.Connectors
+is
+
+   procedure Post_Reset_Off is null;
+   procedure Initialize is null;
+
+   ----------------------------------------------------------------------------
+
+   procedure Pre_On
+     (Pipe        : in     Pipe_Index;
+      Port_Cfg    : in     Port_Config;
+      PLL_Hint    : in     Word32;
+      Success     :    out Boolean)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+      Success := True;
+   end Pre_On;
+
+   procedure Post_On
+     (Pipe     : in     Pipe_Index;
+      Port_Cfg : in     Port_Config;
+      PLL_Hint : in     Word32;
+      Success  :    out Boolean)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+      Success := True;
+      if Port_Cfg.Port = LVDS then
+         GMCH.LVDS.On (Port_Cfg, Pipe);
+      elsif Port_Cfg.Port = VGA then
+         GMCH.VGA.On (Pipe, Port_Cfg.Mode);
+      end if;
+
+      Panel.On (Port_Cfg.Panel, Wait => False);
+      Panel.Backlight_On (Port_Cfg.Panel);
+   end Post_On;
+
+   ----------------------------------------------------------------------------
+
+   procedure Pre_Off (Port_Cfg : Port_Config)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      Panel.Backlight_Off (Port_Cfg.Panel);
+      Panel.Off (Port_Cfg.Panel);
+   end Pre_Off;
+
+   procedure Post_Off (Port_Cfg : Port_Config)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+      if Port_Cfg.Port = LVDS then
+         GMCH.LVDS.Off;
+      elsif Port_Cfg.Port = VGA then
+         GMCH.VGA.Off;
+      end if;
+   end Post_Off;
+
+   ----------------------------------------------------------------------------
+
+   procedure Pre_All_Off
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      for P in Valid_Panels loop
+         Panel.Backlight_Off (P);
+         Panel.Off (P);
+      end loop;
+   end Pre_All_Off;
+
+   procedure Post_All_Off
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+      GMCH.LVDS.Off;
+      GMCH.VGA.Off;
+   end Post_All_Off;
+
+end HW.GFX.GMA.Connectors;
diff --git a/common/i945/hw-gfx-gma-gmch-lvds.adb b/common/i945/hw-gfx-gma-gmch-lvds.adb
new file mode 100644
index 0000000..4d57041
--- /dev/null
+++ b/common/i945/hw-gfx-gma-gmch-lvds.adb
@@ -0,0 +1,86 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
+
+with HW.Debug;
+with GNAT.Source_Info;
+
+package body HW.GFX.GMA.GMCH.LVDS is
+
+   LVDS_ENABLE                     : constant :=  1 * 2 ** 31;
+   LVDS_DITHER_EN                  : constant :=  1 * 2 ** 25;
+   LVDS_VSYNC_POLARITY_INVERT      : constant :=  1 * 2 ** 21;
+   LVDS_HSYNC_POLARITY_INVERT      : constant :=  1 * 2 ** 20;
+   LVDS_CLK_A_DATA_A0A2_POWER_MASK : constant :=  3 * 2 **  8;
+   LVDS_CLK_A_DATA_A0A2_POWER_DOWN : constant :=  0 * 2 **  8;
+   LVDS_CLK_A_DATA_A0A2_POWER_UP   : constant :=  3 * 2 **  8;
+   LVDS_CLK_B_POWER_MASK           : constant :=  3 * 2 **  4;
+   LVDS_CLK_B_POWER_DOWN           : constant :=  0 * 2 **  4;
+   LVDS_CLK_B_POWER_UP             : constant :=  3 * 2 **  4;
+   LVDS_DATA_B0B2_POWER_MASK       : constant :=  3 * 2 **  2;
+   LVDS_DATA_B0B2_POWER_DOWN       : constant :=  0 * 2 **  2;
+   LVDS_DATA_B0B2_POWER_UP         : constant :=  3 * 2 **  2;
+
+   ----------------------------------------------------------------------------
+
+   procedure On (Port_Cfg : in Port_Config;
+                 Pipe     : in Pipe_Index)
+   is
+      -- Pre-i965: LVDS encoder can only source from Pipe B (Secondary).
+      LVDS_Pipe : constant Pipe_Index :=
+        (if Config.LVDS_Needs_Pipe_B then Secondary else Pipe);
+
+      Sync_Polarity : constant Word32 :=
+        (if Port_Cfg.Mode.H_Sync_Active_High then 0
+         else LVDS_HSYNC_POLARITY_INVERT) or
+        (if Port_Cfg.Mode.V_Sync_Active_High then 0
+         else LVDS_VSYNC_POLARITY_INVERT);
+
+      Two_Channel : constant Word32 :=
+        (if Port_Cfg.Mode.Dotclock >= Config.LVDS_Dual_Threshold then
+            LVDS_CLK_B_POWER_UP or LVDS_DATA_B0B2_POWER_UP else 0);
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+      pragma Debug (Port_Cfg.Mode.BPC /= 6, Debug.Put_Line
+        ("WARNING: Only 18bpp LVDS mode implemented."));
+      pragma Debug (Config.LVDS_Needs_Pipe_B and Pipe /= Secondary,
+        Debug.Put_Line ("WARNING: Forcing LVDS to Pipe B (pre-i965 requirement)."));
+
+      Registers.Write
+        (Register => Registers.GMCH_LVDS,
+         Value    => LVDS_ENABLE or
+                     GMCH_PORT_PIPE_SELECT (LVDS_Pipe) or
+                     Sync_Polarity or
+                     LVDS_CLK_A_DATA_A0A2_POWER_UP or
+                     Two_Channel or
+                     LVDS_DITHER_EN);
+   end On;
+
+   ----------------------------------------------------------------------------
+
+   procedure Off
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      Registers.Write
+        (Register => Registers.GMCH_LVDS,
+         Value    => LVDS_CLK_A_DATA_A0A2_POWER_DOWN or
+                     LVDS_CLK_B_POWER_DOWN or
+                     LVDS_DATA_B0B2_POWER_DOWN);
+   end Off;
+
+end HW.GFX.GMA.GMCH.LVDS;
diff --git a/common/i945/hw-gfx-gma-gmch-lvds.ads b/common/i945/hw-gfx-gma-gmch-lvds.ads
new file mode 100644
index 0000000..04c6719
--- /dev/null
+++ b/common/i945/hw-gfx-gma-gmch-lvds.ads
@@ -0,0 +1,23 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+package HW.GFX.GMA.GMCH.LVDS
+is
+
+   procedure On (Port_Cfg : in Port_Config;
+                 Pipe     : in Pipe_Index);
+
+   procedure Off;
+
+end HW.GFX.GMA.GMCH.LVDS;
diff --git a/common/i945/hw-gfx-gma-gmch-vga.adb b/common/i945/hw-gfx-gma-gmch-vga.adb
new file mode 100644
index 0000000..1a43b64
--- /dev/null
+++ b/common/i945/hw-gfx-gma-gmch-vga.adb
@@ -0,0 +1,80 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
+
+with HW.Debug;
+with GNAT.Source_Info;
+
+use type HW.Word64;
+
+package body HW.GFX.GMA.GMCH.VGA is
+
+   ADPA_DAC_ENABLE         : constant := 1 * 2 ** 31;
+   ADPA_USE_VGA_HVPOLARITY : constant := 1 * 2 ** 15;
+   ADPA_VSYNC_DISABLE      : constant := 1 * 2 ** 11;
+   ADPA_HSYNC_DISABLE      : constant := 1 * 2 ** 10;
+   ADPA_VSYNC_ACTIVE_HIGH  : constant := 1 * 2 **  4;
+   ADPA_HSYNC_ACTIVE_HIGH  : constant := 1 * 2 **  3;
+
+   ADPA_MASK : constant Word32 :=
+      GMCH_PORT_PIPE_SELECT_MASK or
+      ADPA_DAC_ENABLE        or
+      ADPA_VSYNC_DISABLE     or
+      ADPA_HSYNC_DISABLE     or
+      ADPA_VSYNC_ACTIVE_HIGH or
+      ADPA_HSYNC_ACTIVE_HIGH or
+      ADPA_USE_VGA_HVPOLARITY;
+
+   ----------------------------------------------------------------------------
+
+   procedure On
+     (Pipe     : Pipe_Index;
+      Mode     : in Mode_Type)
+   is
+      Polarity : Word32 := 0;
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      if Mode.H_Sync_Active_High then
+         Polarity := Polarity or ADPA_HSYNC_ACTIVE_HIGH;
+      end if;
+      if Mode.V_Sync_Active_High then
+         Polarity := Polarity or ADPA_VSYNC_ACTIVE_HIGH;
+      end if;
+
+      Registers.Unset_And_Set_Mask
+        (Register    => Registers.GMCH_ADPA,
+         Mask_Unset  => ADPA_MASK,
+         Mask_Set    => ADPA_DAC_ENABLE or
+                        GMCH_PORT_PIPE_SELECT (Pipe) or
+                        Polarity);
+   end On;
+
+   ----------------------------------------------------------------------------
+
+   procedure Off
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      Registers.Unset_And_Set_Mask
+        (Register    => Registers.GMCH_ADPA,
+         Mask_Unset  => ADPA_DAC_ENABLE,
+         Mask_Set    => ADPA_HSYNC_DISABLE or
+                        ADPA_VSYNC_DISABLE);
+   end Off;
+
+end HW.GFX.GMA.GMCH.VGA;
diff --git a/common/i945/hw-gfx-gma-gmch-vga.ads b/common/i945/hw-gfx-gma-gmch-vga.ads
new file mode 100644
index 0000000..e7b6e7c
--- /dev/null
+++ b/common/i945/hw-gfx-gma-gmch-vga.ads
@@ -0,0 +1,22 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+package HW.GFX.GMA.GMCH.VGA is
+
+   procedure On
+     (Pipe : in Pipe_Index;
+      Mode : in Mode_Type);
+   procedure Off;
+
+end HW.GFX.GMA.GMCH.VGA;
diff --git a/common/i945/hw-gfx-gma-gmch.ads b/common/i945/hw-gfx-gma-gmch.ads
new file mode 100644
index 0000000..740913e
--- /dev/null
+++ b/common/i945/hw-gfx-gma-gmch.ads
@@ -0,0 +1,27 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+
+private package HW.GFX.GMA.GMCH is
+
+   GMCH_PORT_PIPE_SELECT_SHIFT : constant := 30;
+   GMCH_PORT_PIPE_SELECT_MASK  : constant := 1 * 2 ** 30;
+   type GMCH_PORT_PIPE_SELECT_Array is array (Pipe_Index) of Word32;
+   GMCH_PORT_PIPE_SELECT       : constant GMCH_PORT_PIPE_SELECT_Array :=
+     (Primary   => 0 * 2 ** GMCH_PORT_PIPE_SELECT_SHIFT,
+      Secondary => 1 * 2 ** GMCH_PORT_PIPE_SELECT_SHIFT,
+      Tertiary  => 0);
+
+end HW.GFX.GMA.GMCH;
diff --git a/common/i945/hw-gfx-gma-plls.adb b/common/i945/hw-gfx-gma-plls.adb
new file mode 100644
index 0000000..96ba6af
--- /dev/null
+++ b/common/i945/hw-gfx-gma-plls.adb
@@ -0,0 +1,562 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.Time;
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
+
+with HW.Debug;
+with GNAT.Source_Info;
+
+package body HW.GFX.GMA.PLLs
+with
+   Refined_State => (State => PLLs)
+is
+
+   Debug_Clocks : constant Boolean := False;
+
+   type Count_Range is new Natural range 0 .. 2;
+
+   type PLL_State is record
+      Use_Count   : Count_Range;
+      Mode        : Mode_Type;
+   end record;
+
+   type PLL_State_Array is array (DPLLs) of PLL_State;
+
+   PLLs : PLL_State_Array;
+
+   ----------------------------------------------------------------------------
+
+   -- i945 PLL limits use "actual" divider values (register_value + 2).
+   -- Linux kernel i9xx limits (register values):
+   --   N: 1..6, M1: 8..18, M2: 3..7
+   -- Actual values (register + 2):
+   --   N: 3..8, M1: 10..20, M2: 5..9
+   -- M = 5 * M1 + M2, range 70..120
+   -- VCO = ref_clk * M / N, range 1400..2800 MHz
+   -- Reference clock: 96 MHz
+
+   subtype N_Range     is Int64 range          3 ..          8;
+   subtype M_Range     is Int64 range         70 ..        120;
+   subtype M1_Range    is Int64 range         10 ..         20;
+   subtype M2_Range    is Int64 range          5 ..          9;
+   subtype P_Range     is Int64 range          5 ..         98;
+   subtype P1_Range    is Int64 range          1 ..          8;
+   subtype P2_Range    is Int64 range          5 ..         14;
+   subtype VCO_Range   is Int64 range 1400000000 .. 2800000000;
+   subtype Clock_Range is HW.GFX.Frequency_Type;
+
+   type Clock_Type is
+      record
+         N               : N_Range;
+         M1              : M1_Range;
+         M2              : M2_Range;
+         P1              : P1_Range;
+         P2              : P2_Range;
+         M               : M_Range;
+         P               : P_Range;
+         VCO             : VCO_Range;
+         Reference_Clock : Clock_Range;
+         Dotclock        : Clock_Range;
+      end record;
+
+   Invalid_Clock : constant Clock_Type := Clock_Type'
+      (N               => N_Range'Last,
+       M1              => M1_Range'Last,
+       M2              => M2_Range'Last,
+       P1              => P1_Range'Last,
+       P2              => P2_Range'Last,
+       Reference_Clock => Clock_Range'Last,
+       M               => M_Range'Last,
+       P               => P_Range'Last,
+       VCO             => VCO_Range'Last,
+       Dotclock        => Clock_Range'Last);
+
+   type Limits_Type is
+      record
+         N_Lower      : N_Range;
+         N_Upper      : N_Range;
+         M_Lower      : M_Range;
+         M_Upper      : M_Range;
+         M1_Lower     : M1_Range;
+         M1_Upper     : M1_Range;
+         M2_Lower     : M2_Range;
+         M2_Upper     : M2_Range;
+         P_Lower      : P_Range;
+         P_Upper      : P_Range;
+         P1_Lower     : P1_Range;
+         P1_Upper     : P1_Range;
+         P2_Fast      : P2_Range;
+         P2_Slow      : P2_Range;
+         P2_Threshold : Clock_Range;
+         VCO_Lower    : VCO_Range;
+         VCO_Upper    : VCO_Range;
+      end record;
+
+   -- i9xx LVDS limits (from Linux intel_limits_i9xx_lvds)
+   LVDS_Limits : constant Limits_Type := Limits_Type'
+     (N_Lower      =>   3,           N_Upper   =>   8,
+      M_Lower      =>  70,           M_Upper   => 120,
+      M1_Lower     =>  10,           M1_Upper  =>  20,
+      M2_Lower     =>   5,           M2_Upper  =>   9,
+      P_Lower      =>   7,           P_Upper   =>  98,
+      P1_Lower     =>   1,           P1_Upper  =>   8,
+      P2_Fast      =>   7,           P2_Slow   =>  14,
+      P2_Threshold => 112_000_000,
+      VCO_Lower    => 1_400_000_000, VCO_Upper => 2_800_000_000);
+
+   -- i9xx SDVO/DAC limits (from Linux intel_limits_i9xx_sdvo)
+   SDVO_DAC_Limits : constant Limits_Type := Limits_Type'
+     (N_Lower      =>   3,           N_Upper   =>   8,
+      M_Lower      =>  70,           M_Upper   => 120,
+      M1_Lower     =>  10,           M1_Upper  =>  20,
+      M2_Lower     =>   5,           M2_Upper  =>   9,
+      P_Lower      =>   5,           P_Upper   =>  80,
+      P1_Lower     =>   1,           P1_Upper  =>   8,
+      -- use P2_Slow if Dotclock <= P2_Threshold, P2_Fast otherwise
+      P2_Fast      =>   5,           P2_Slow   =>  10,
+      P2_Threshold => 200_000_000,
+      VCO_Lower    => 1_400_000_000, VCO_Upper => 2_800_000_000);
+
+   ----------------------------------------------------------------------------
+
+   type Regs is array (DPLLs) of Registers.Registers_Index;
+
+   DPLL : constant Regs := Regs'(Registers.GMCH_DPLL_A, Registers.GMCH_DPLL_B);
+   DPLL_VCO_ENABLE         : constant := 1 * 2 ** 31;
+   DPLL_VGA_MODE_DIS       : constant := 1 * 2 ** 28;
+   DPLL_P2_10_OR_14        : constant := 0 * 2 ** 24;
+   DPLL_P2_5_OR_7          : constant := 1 * 2 ** 24;
+   DPLL_P1_DIVIDER_SHIFT   : constant := 16;
+   DPLL_SDVOCLK            : constant := 2 * 2 ** 13;
+
+   -- i945 does not use DPLL_PULSE_PHASE (bits 12:9, Gen4+ only)
+   -- i945 uses DVO_2X_MODE (bit 30) for SDVO outputs (same bit as
+   -- DPLL_SDVO_HIGH_SPEED on Gen4+)
+   DPLL_DVO_2X_MODE : constant := 1 * 2 ** 30;
+   DPLL_MODE_LVDS   : constant := 2 * 2 ** 26;
+   DPLL_MODE_DAC    : constant := 1 * 2 ** 26;
+   DPLL_DREFCLK     : constant := 0 * 2 ** 13;
+   DPLL_SSC         : constant := 3 * 2 ** 13;
+
+   MODE_DPLL_DAC : constant Word32 := Word32'
+     (DPLL_MODE_DAC or DPLL_DREFCLK);
+
+   MODE_DPLL_SDVO : constant Word32 := Word32'
+     (DPLL_MODE_DAC or DPLL_DREFCLK or DPLL_DVO_2X_MODE);
+
+   MODE_DPLL_LVDS : constant Word32 := Word32'
+      (DPLL_MODE_LVDS or DPLL_SSC);
+
+   type DPLL_Mode_Array is array (Display_Type) of Word32;
+
+   DPLL_Mode : constant DPLL_Mode_Array := DPLL_Mode_Array'
+     (LVDS     => MODE_DPLL_LVDS,
+      VGA      => MODE_DPLL_DAC,
+      HDMI     => MODE_DPLL_SDVO,  -- SDVO outputs use HDMI display type
+      Others   => MODE_DPLL_SDVO);
+
+   FP0 : constant Regs := Regs'(Registers.GMCH_FPA0, Registers.GMCH_FPB0);
+   FP1 : constant Regs := Regs'(Registers.GMCH_FPA1, Registers.GMCH_FPB1);
+   FP_N_SHIFT            : constant := 16;
+   FP_M1_SHIFT           : constant := 8;
+   FP_M2_SHIFT           : constant := 0;
+
+   ----------------------------------------------------------------------------
+
+   procedure Verify_Parameters
+      (N               : in     N_Range;
+       M1              : in     M1_Range;
+       M2              : in     M2_Range;
+       P1              : in     P1_Range;
+       P2              : in     P2_Range;
+       Reference_Clock : in     Clock_Range;
+       Current_Limits  : in     Limits_Type;
+       Result          :    out Clock_Type;
+       Valid           :    out Boolean)
+   with
+      Global => null,
+      Pre => True,
+      Post => True
+   is
+      M        : Int64;
+      P        : Int64;
+      VCO      : Int64;
+      Dotclock : Int64;
+   begin
+      pragma Debug (Debug_Clocks, Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      M        := 5 * M1 + M2;
+      P        := P1 * P2;
+      VCO      := (Int64 (Reference_Clock) * M) / N;
+      Dotclock := VCO / P;
+
+      pragma Debug (Debug_Clocks and not (Current_Limits.P1_Lower  <= P1  and P1  <= Current_Limits.P1_Upper ), Debug.Put_Line ("P1 out of range."));
+      pragma Debug (Debug_Clocks and     (Current_Limits.P2_Fast   /= P2  and P2  /= Current_Limits.P2_Slow  ), Debug.Put_Line ("P2 out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.P_Lower   <= P   and P   <= Current_Limits.P_Upper  ), Debug.Put_Line ("P out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.M1_Lower  <= M1  and M1  <= Current_Limits.M1_Upper ), Debug.Put_Line ("M1 out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.M2_Lower  <= M2  and M2  <= Current_Limits.M2_Upper ), Debug.Put_Line ("M2 out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.N_Lower   <= N   and N   <= Current_Limits.N_Upper  ), Debug.Put_Line ("N out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.M_Lower   <= M   and M   <= Current_Limits.M_Upper  ), Debug.Put_Line ("M out of range."));
+      pragma Debug (Debug_Clocks and not (Current_Limits.VCO_Lower <= VCO and VCO <= Current_Limits.VCO_Upper), Debug.Put_Line ("VCO out of range."));
+
+      pragma Debug (Debug_Clocks and not (Int64 (Clock_Range'First) <= Dotclock),       Debug.Put_Line ("Dotclock too low."));
+      pragma Debug (Debug_Clocks and not (Int64 (Clock_Range'First) <= Dotclock),       Debug.Put_Int64 (Dotclock));
+      pragma Debug (Debug_Clocks and not (Int64 (Clock_Range'First) <= Dotclock),       Debug.New_Line);
+
+      pragma Debug (Debug_Clocks and not (Dotclock <= Int64 (Clock_Range'Last)),        Debug.Put_Line ("Dotclock too high."));
+      pragma Debug (Debug_Clocks and not (Dotclock <= Int64 (Clock_Range'Last)),        Debug.Put_Int64 (Dotclock));
+      pragma Debug (Debug_Clocks and not (Dotclock <= Int64 (Clock_Range'Last)),        Debug.New_Line);
+
+      Valid :=
+         Current_Limits.P1_Lower  <= P1  and P1  <= Current_Limits.P1_Upper  and
+         (Current_Limits.P2_Fast   = P2   or P2   = Current_Limits.P2_Slow)  and
+         Current_Limits.P_Lower   <= P   and P   <= Current_Limits.P_Upper   and
+         Current_Limits.M1_Lower  <= M1  and M1  <= Current_Limits.M1_Upper  and
+         Current_Limits.M2_Lower  <= M2  and M2  <= Current_Limits.M2_Upper  and
+         Current_Limits.N_Lower   <= N   and N   <= Current_Limits.N_Upper   and
+         Current_Limits.M_Lower   <= M   and M   <= Current_Limits.M_Upper   and
+         Current_Limits.VCO_Lower <= VCO and VCO <= Current_Limits.VCO_Upper and
+         Int64 (Clock_Range'First) <= Dotclock                               and
+         Dotclock <= Int64 (Clock_Range'Last);
+
+      if Valid
+      then
+         Result := Clock_Type'
+            (N               => N,
+             M1              => M1,
+             M2              => M2,
+             P1              => P1,
+             P2              => P2,
+             Reference_Clock => Reference_Clock,
+             M               => M,
+             P               => P,
+             VCO             => VCO,
+             Dotclock        => Clock_Range (Dotclock));
+      else
+         Result := Invalid_Clock;
+      end if;
+
+   end Verify_Parameters;
+
+   procedure Calculate_Clock_Parameters
+     (Display         : in     Display_Type;
+      Target_Dotclock : in     Clock_Range;
+      Reference_Clock : in     Clock_Range;
+      Best_Clock      :    out Clock_Type;
+      Valid           :    out Boolean)
+   with
+     Global => null,
+     Pre => True,
+     Post => True
+   is
+      Limits : constant Limits_Type :=
+      (case Display is
+          when LVDS   => LVDS_Limits,
+          when others => SDVO_DAC_Limits);
+
+      P2               : P2_Range;
+      Best_Delta       : Int64 := Int64'Last;
+      Current_Delta    : Int64;
+      Current_Clock    : Clock_Type;
+      Registers_Valid  : Boolean;
+   begin
+      pragma Debug (Debug_Clocks, Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      Valid      := False;
+      Best_Clock := Invalid_Clock;
+
+      if Target_Dotclock <= Limits.P2_Threshold then
+         P2 := Limits.P2_Slow;
+      else
+         P2 := Limits.P2_Fast;
+      end if;
+
+      for N in N_Range range Limits.N_Lower .. Limits.N_Upper
+      loop
+         -- reverse loops as hardware prefers higher values
+         for M1 in reverse M1_Range range Limits.M1_Lower .. Limits.M1_Upper
+         loop
+            pragma Loop_Invariant (True);
+            for M2 in reverse M2_Range range Limits.M2_Lower .. Int64'Min (Limits.M2_Upper, M1)
+            loop
+               pragma Loop_Invariant (True);
+               for P1 in reverse P1_Range range Limits.P1_Lower .. Limits.P1_Upper
+               loop
+                  Verify_Parameters
+                    (N               => N,
+                     M1              => M1,
+                     M2              => M2,
+                     P1              => P1,
+                     P2              => P2,
+                     Reference_Clock => Reference_Clock,
+                     Current_Limits  => Limits,
+                     Result          => Current_Clock,
+                     Valid           => Registers_Valid);
+
+                  if Registers_Valid
+                  then
+                     if Current_Clock.Dotclock > Target_Dotclock
+                     then
+                        Current_Delta := Current_Clock.Dotclock - Target_Dotclock;
+                     else
+                        Current_Delta := Target_Dotclock - Current_Clock.Dotclock;
+                     end if;
+
+                     if Current_Delta < Best_Delta
+                     then
+                        Best_Delta := Current_Delta;
+                        Best_Clock := Current_Clock;
+                        Valid      := True;
+                     end if;
+
+                     pragma Debug (Debug_Clocks, Debug.Put ("Current/Target/Best_Delta: "));
+                     pragma Debug (Debug_Clocks, Debug.Put_Int64 (Current_Clock.Dotclock));
+                     pragma Debug (Debug_Clocks, Debug.Put ("/"));
+                     pragma Debug (Debug_Clocks, Debug.Put_Int64 (Target_Dotclock));
+                     pragma Debug (Debug_Clocks, Debug.Put ("/"));
+                     pragma Debug (Debug_Clocks, Debug.Put_Int64 (Best_Delta));
+                     pragma Debug (Debug_Clocks, Debug.Put_Line ("."));
+
+                  end if;
+               end loop;
+            end loop;
+         end loop;
+      end loop;
+
+      pragma Debug (Valid,     Debug.Put_Line ("Valid clock found."));
+      pragma Debug (Valid,     Debug.Put ("Best/Target/Delta: "));
+      pragma Debug (Valid,     Debug.Put_Int64 (Best_Clock.Dotclock));
+      pragma Debug (Valid,     Debug.Put ("/"));
+      pragma Debug (Valid,     Debug.Put_Int64 (Target_Dotclock));
+      pragma Debug (Valid,     Debug.Put ("/"));
+      pragma Debug (Valid,     Debug.Put_Int64 (Best_Delta));
+      pragma Debug (Valid,     Debug.Put_Line ("."));
+      pragma Debug (not Valid, Debug.Put_Line ("No valid clock found."));
+
+   end Calculate_Clock_Parameters;
+
+   procedure Program_DPLL
+     (PLL      : DPLLs;
+      Display  : Display_Type;
+      Clk      : Clock_Type)
+   with
+      Global => (In_Out => Registers.Register_State),
+      Pre => True,
+      Post => True
+   is
+      FP, Encoded_P1, Encoded_P2 : Word32;
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      -- FP register: N, M1, M2 encoded as (actual_value - 2)
+      FP :=
+         Shift_Left (Word32 (Clk.N - 2), FP_N_SHIFT)     or
+         Shift_Left (Word32 (Clk.M1 - 2), FP_M1_SHIFT)   or
+         Shift_Left (Word32 (Clk.M2 - 2), FP_M2_SHIFT);
+
+      Registers.Write (FP0 (PLL), FP);
+      Registers.Write (FP1 (PLL), FP);
+
+      -- P1 encoding: bitmask (1 << (P1-1)) shifted into position
+      Encoded_P1 := Shift_Left (1, Natural (Clk.P1) - 1);
+
+      if Clk.P2 = 5 or Clk.P2 = 7
+      then
+         Encoded_P2 := DPLL_P2_5_OR_7;
+      else
+         Encoded_P2 := DPLL_P2_10_OR_14;
+      end if;
+
+      -- i945 DPLL register: no HIGH_SPEED bit, no PULSE_PHASE bits
+      Registers.Write
+        (Register => DPLL (PLL),
+         Value    => DPLL_Mode (Display)                            or
+                     DPLL_VGA_MODE_DIS                              or
+                     Encoded_P2                                     or
+                     Shift_Left (Encoded_P1, DPLL_P1_DIVIDER_SHIFT));
+   end Program_DPLL;
+
+   procedure On
+     (PLL      : in     T;
+      Port_Cfg : in     Port_Config;
+      Success  :    out Boolean)
+   is
+      Target_Clock : constant Frequency_Type := Port_Cfg.Mode.Dotclock;
+      Clk : Clock_Type;
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      Success := PLL in DPLLs;
+      Clk := Invalid_Clock;
+
+      if Success then
+         if Target_Clock <= 400_000_000 then
+            Calculate_Clock_Parameters
+              (Display           => Port_Cfg.Display,
+               Target_Dotclock   => Target_Clock,
+               Reference_Clock   => 96_000_000,
+               Best_Clock        => Clk,
+               Valid             => Success);
+         else
+            Success := False;
+            pragma Debug (Debug.Put ("WARNING: Targeted clock too high: "));
+            pragma Debug (Debug.Put_Int64 (Target_Clock));
+            pragma Debug (Debug.Put (" > "));
+            pragma Debug (Debug.Put_Int32 (400_000_000));
+            pragma Debug (Debug.New_Line);
+            pragma Debug (Debug.New_Line);
+         end if;
+      end if;
+
+      if Success then
+         Program_DPLL (PLL, Port_Cfg.Display, Clk);
+
+         Registers.Set_Mask (DPLL (PLL), DPLL_VCO_ENABLE);
+         Registers.Posting_Read (DPLL (PLL));
+         Time.U_Delay (150);
+      end if;
+   end On;
+
+   procedure Off (PLL : T)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      if PLL in DPLLs then
+         Registers.Unset_Mask (DPLL (PLL), DPLL_VCO_ENABLE);
+      end if;
+   end Off;
+
+   ----------------------------------------------------------------------------
+
+   procedure Initialize
+   is
+   begin
+      PLLs :=
+        (DPLLs =>
+           (Use_Count   => 0,
+            Mode        => Invalid_Mode));
+   end Initialize;
+
+   procedure Alloc_Configurable
+     (Port_Cfg : in     Port_Config;
+      PLL      :    out T;
+      Success  :    out Boolean)
+   with
+      Pre => True
+   is
+      function Config_Matches (PE : PLL_State) return Boolean
+      is
+      begin
+         return PE.Mode = Port_Cfg.Mode;
+      end Config_Matches;
+   begin
+      -- try to find shareable PLL
+      for P in DPLLs loop
+         Success := PLLs (P).Use_Count /= 0 and
+                     PLLs (P).Use_Count /= Count_Range'Last and
+                     Config_Matches (PLLs (P));
+         if Success then
+            PLL := P;
+            PLLs (PLL).Use_Count := PLLs (PLL).Use_Count + 1;
+            return;
+         end if;
+      end loop;
+
+      -- try to find free PLL
+      for P in DPLLs loop
+         if PLLs (P).Use_Count = 0 then
+            PLL := P;
+            On (PLL, Port_Cfg, Success);
+            if Success then
+               PLLs (PLL) :=
+                 (Use_Count   => 1,
+                  Mode        => Port_Cfg.Mode);
+            end if;
+            return;
+         end if;
+      end loop;
+
+      PLL := Invalid;
+   end Alloc_Configurable;
+
+   procedure Alloc
+     (Port_Cfg : in     Port_Config;
+      PLL      :    out T;
+      Success  :    out Boolean)
+   is
+      -- On i945, DPLL-to-pipe mapping is fixed:
+      -- LVDS -> Pipe B -> DPLL_B, all others -> Pipe A -> DPLL_A
+      Target : constant DPLLs :=
+        (if Port_Cfg.Display = LVDS then DPLL_B else DPLL_A);
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      PLL := Target;
+
+      if PLLs (Target).Use_Count /= 0 and
+         PLLs (Target).Use_Count /= Count_Range'Last and
+         PLLs (Target).Mode = Port_Cfg.Mode
+      then
+         -- Share existing PLL with matching mode
+         PLLs (Target).Use_Count := PLLs (Target).Use_Count + 1;
+         Success := True;
+      elsif PLLs (Target).Use_Count = 0 then
+         -- Program the PLL
+         On (Target, Port_Cfg, Success);
+         if Success then
+            PLLs (Target) :=
+              (Use_Count   => 1,
+               Mode        => Port_Cfg.Mode);
+         end if;
+      else
+         PLL := Invalid;
+         Success := False;
+      end if;
+   end Alloc;
+
+   procedure Free (PLL : T)
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      if PLL in DPLLs then
+         if PLLs (PLL).Use_Count /= 0 then
+            PLLs (PLL).Use_Count := PLLs (PLL).Use_Count - 1;
+            if PLLs (PLL).Use_Count = 0 then
+               Off (PLL);
+            end if;
+         end if;
+      end if;
+   end Free;
+
+   procedure All_Off
+   is
+   begin
+      pragma Debug (Debug.Put_Line (GNAT.Source_Info.Enclosing_Entity));
+
+      for PLL in DPLLs loop
+         Off (PLL);
+      end loop;
+   end All_Off;
+
+   function Register_Value (PLL : T) return Word32
+   is
+   begin
+      return (if PLL = DPLL_B then 1 else 0);
+   end Register_Value;
+
+end HW.GFX.GMA.PLLs;
diff --git a/common/i945/hw-gfx-gma-plls.ads b/common/i945/hw-gfx-gma-plls.ads
new file mode 100644
index 0000000..f99a29f
--- /dev/null
+++ b/common/i945/hw-gfx-gma-plls.ads
@@ -0,0 +1,40 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+private package HW.GFX.GMA.PLLs
+with
+   Abstract_State => (State with Part_Of => GMA.State)
+is
+
+   -- XXX: Types should be private (but that triggers a bug in SPARK GPL 2016)
+   type T is (Invalid_PLL, DPLL_A, DPLL_B);
+   subtype DPLLs is T range DPLL_A .. DPLL_B;
+   Invalid : constant T := Invalid_PLL;
+
+   procedure Initialize
+   with
+      Global => (Output => State);
+
+   procedure Alloc
+     (Port_Cfg : in     Port_Config;
+      PLL      :    out T;
+      Success  :    out Boolean);
+
+   procedure Free (PLL : T);
+
+   procedure All_Off;
+
+   function Register_Value (PLL : T) return Word32;
+
+end HW.GFX.GMA.PLLs;
diff --git a/common/i945/hw-gfx-gma-port_detect.adb b/common/i945/hw-gfx-gma-port_detect.adb
new file mode 100644
index 0000000..af82c17
--- /dev/null
+++ b/common/i945/hw-gfx-gma-port_detect.adb
@@ -0,0 +1,68 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
+
+package body HW.GFX.GMA.Port_Detect
+is
+
+   CRT_HOTPLUG_INT_EN               : constant := 1 * 2 ** 9;
+   CRT_HOTPLUG_ACTIVATION_PERIOD_64 : constant := 1 * 2 ** 8;
+
+   HOTPLUG_INT_STATUS : constant array (Active_Port_Type) of Word32 :=
+     (Analog => 1 * 2 ** 11,
+      others => 0);
+
+   procedure Initialize
+   is
+   begin
+      -- i945: VGA (ADPA) is always present
+      Config.Valid_Port (Analog) := True;
+
+      -- LVDS is only present on mobile (i945GM)
+      Config.Valid_Port (LVDS)   := Config.GMCH_I945GM;
+
+      -- Enable CRT hotplug detection
+      Registers.Write
+        (Register => Registers.PORT_HOTPLUG_EN,
+         Value    => CRT_HOTPLUG_INT_EN or CRT_HOTPLUG_ACTIVATION_PERIOD_64);
+   end Initialize;
+
+   procedure Hotplug_Detect (Port : in Active_Port_Type; Detected : out Boolean)
+   is
+      Ctl32 : Word32;
+   begin
+      Registers.Read (Register => Registers.PORT_HOTPLUG_STAT,
+                      Value    => Ctl32);
+      Detected := (Ctl32 and HOTPLUG_INT_STATUS (Port)) /= 0;
+
+      if Detected then
+         Registers.Set_Mask
+           (Register => Registers.PORT_HOTPLUG_STAT,
+            Mask     => HOTPLUG_INT_STATUS (Port));
+      end if;
+   end Hotplug_Detect;
+
+   procedure Clear_Hotplug_Detect (Port : Active_Port_Type)
+   is
+      Ignored_HPD : Boolean;
+   begin
+      pragma Warnings (GNATprove, Off, "unused assignment to ""Ignored_HPD""",
+                       Reason => "We want to clear pending events only");
+      Port_Detect.Hotplug_Detect (Port, Ignored_HPD);
+      pragma Warnings (GNATprove, On, "unused assignment to ""Ignored_HPD""");
+   end Clear_Hotplug_Detect;
+
+end HW.GFX.GMA.Port_Detect;
diff --git a/common/i945/hw-gfx-gma-power_and_clocks.adb b/common/i945/hw-gfx-gma-power_and_clocks.adb
new file mode 100644
index 0000000..e18d1e3
--- /dev/null
+++ b/common/i945/hw-gfx-gma-power_and_clocks.adb
@@ -0,0 +1,131 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
+
+package body HW.GFX.GMA.Power_And_Clocks is
+
+   CLKCFG_FSB_MASK   : constant := 7 * 2 ** 0;
+   -- hrawclk = FSB / 4. The CLKCFG FSB bits encode different FSB speeds
+   -- depending on whether the platform is mobile or desktop (ALT encoding):
+   --   mobile:  0=FSB400,  1=FSB533, 2=FSB800, 3=FSB667, 6=FSB1067, 7=FSB1333
+   --   desktop: 0=FSB1067, 1=FSB533, 2=FSB800, 3=FSB667, 4=FSB1333,
+   --            5=FSB400,  6=FSB1600
+   HRAWCLK_100       : constant Frequency_Type := 100_000_000;  -- FSB 400
+   HRAWCLK_133       : constant Frequency_Type := 133_333_333;  -- FSB 533
+   HRAWCLK_167       : constant Frequency_Type := 166_666_666;  -- FSB 667
+   HRAWCLK_200       : constant Frequency_Type := 200_000_000;  -- FSB 800
+   HRAWCLK_267       : constant Frequency_Type := 266_666_667;  -- FSB 1067
+   HRAWCLK_333       : constant Frequency_Type := 333_333_333;  -- FSB 1333
+   HRAWCLK_400       : constant Frequency_Type := 400_000_000;  -- FSB 1600 (desktop)
+
+   -- i945 CDClk values (from Linux kernel intel_cdclk.c):
+   --
+   -- i945G (desktop): fixed 400 MHz CDClk
+   --
+   -- i945GM (mobile): read from GCFGC PCI register (0xF0)
+   --   GC_LOW_FREQUENCY_ENABLE (bit 7) => 133 MHz
+   --   GC_DISPLAY_CLOCK_190_200_MHZ (0 << 4) => 200 MHz (default)
+   --   GC_DISPLAY_CLOCK_333_320_MHZ (4 << 4) => 320 MHz
+
+   procedure Get_CDClk (CDClk : out Config.CDClk_Range)
+   is
+      use type HW.Word16;
+
+      GC_LOW_FREQUENCY_ENABLE      : constant Word16 := 1 * 2 ** 7;
+      GC_DISPLAY_CLOCK_MASK        : constant Word16 := 7 * 2 ** 4;
+      GC_DISPLAY_CLOCK_320_MHZ     : constant Word16 := 4 * 2 ** 4;
+
+      GCFGC : Word16;
+      Tmp_Clk : Frequency_Type := 200_000_000;
+   begin
+      if Config.GMCH_I945GM then
+         if PCI_Usable then
+            PCI_Read16 (GCFGC, 16#f0#);
+            if (GCFGC and GC_LOW_FREQUENCY_ENABLE) /= 0 then
+               Tmp_Clk := 133_333_333;
+            elsif (GCFGC and GC_DISPLAY_CLOCK_MASK) = GC_DISPLAY_CLOCK_320_MHZ then
+               Tmp_Clk := 320_000_000;
+            else
+               Tmp_Clk := 200_000_000;
+            end if;
+         end if;
+      else
+         -- i945G desktop: fixed 400 MHz
+         Tmp_Clk := 400_000_000;
+      end if;
+
+      if Tmp_Clk in Config.CDClk_Range then
+         CDClk := Tmp_Clk;
+      else
+         CDClk := 200_000_000;
+      end if;
+   end Get_CDClk;
+
+   -- hrawclk = FSB / 4. The CLKCFG FSB encoding differs between
+   -- mobile (i945GM) and desktop (i945G), see CLKCFG_FSB_MASK comment above.
+   procedure Get_Raw_Clock (Raw_Clock : out Frequency_Type)
+   is
+      CLK_CFG : Word32;
+      type Freq_Sel is new Natural range 0 .. 7;
+   begin
+      Registers.Read
+        (Register => Registers.GMCH_CLKCFG,
+         Value => CLK_CFG);
+      if Config.GMCH_I945GM then
+         Raw_Clock := (case Freq_Sel (CLK_CFG and CLKCFG_FSB_MASK) is
+            when 0      => HRAWCLK_100,  -- FSB 400
+            when 1      => HRAWCLK_133,  -- FSB 533
+            when 2      => HRAWCLK_200,  -- FSB 800
+            when 3      => HRAWCLK_167,  -- FSB 667
+            when 6      => HRAWCLK_267,  -- FSB 1067
+            when 7      => HRAWCLK_333,  -- FSB 1333
+            when others => HRAWCLK_133);
+      else
+         Raw_Clock := (case Freq_Sel (CLK_CFG and CLKCFG_FSB_MASK) is
+            when 0      => HRAWCLK_267,  -- FSB 1067
+            when 1      => HRAWCLK_133,  -- FSB 533
+            when 2      => HRAWCLK_200,  -- FSB 800
+            when 3      => HRAWCLK_167,  -- FSB 667
+            when 4      => HRAWCLK_333,  -- FSB 1333
+            when 5      => HRAWCLK_100,  -- FSB 400
+            when 6      => HRAWCLK_400,  -- FSB 1600
+            when others => HRAWCLK_133);
+      end if;
+   end Get_Raw_Clock;
+
+   procedure Initialize
+   is
+      CDClk   : Config.CDClk_Range;
+      Raw_Clk : Frequency_Type;
+   begin
+      Get_CDClk (CDClk);
+      Config.CDClk := CDClk;
+      Config.Max_CDClk := CDClk;
+
+      Get_Raw_Clock (Raw_Clk);
+      Config.Raw_Clock := Raw_Clk;
+   end Initialize;
+
+   procedure Limit_Dotclocks
+     (Configs        : in out Pipe_Configs;
+      CDClk_Switch   :    out Boolean)
+   is
+   begin
+      Config_Helpers.Limit_Dotclocks (Configs, Config.CDClk * 90 / 100);
+      CDClk_Switch := False;
+   end Limit_Dotclocks;
+
+end HW.GFX.GMA.Power_And_Clocks;
diff --git a/common/i945/hw-gfx-gma-power_and_clocks.ads b/common/i945/hw-gfx-gma-power_and_clocks.ads
new file mode 100644
index 0000000..cad2898
--- /dev/null
+++ b/common/i945/hw-gfx-gma-power_and_clocks.ads
@@ -0,0 +1,42 @@
+--
+-- Copyright (C) 2026 Arthur Heymans <arthur@aheymans.xyz>
+--
+-- This program is free software; you can redistribute it and/or modify
+-- it under the terms of the GNU General Public License as published by
+-- the Free Software Foundation; either version 2 of the License, or
+-- (at your option) any later version.
+--
+-- This program is distributed in the hope that it will be useful,
+-- but WITHOUT ANY WARRANTY; without even the implied warranty of
+-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+-- GNU General Public License for more details.
+--
+
+with HW.GFX.GMA.Config_Helpers;
+
+private package HW.GFX.GMA.Power_And_Clocks is
+
+   procedure Initialize;
+
+   procedure Limit_Dotclocks
+     (Configs        : in out Pipe_Configs;
+      CDClk_Switch   :    out Boolean)
+   with
+      Post =>
+         not CDClk_Switch and
+         Config_Helpers.Stable_FB (Configs'Old, Configs);
+   procedure Update_CDClk (Configs : in out Pipe_Configs) is null;
+   procedure Enable_CDClk is null;
+
+   procedure Pre_All_Off is null;
+
+   procedure Post_All_Off is null;
+
+   procedure Power_Set_To (Configs : Pipe_Configs) is null;
+
+   procedure Power_Up (Old_Configs, New_Configs : Pipe_Configs) is null;
+
+   procedure Power_Down (Old_Configs, Tmp_Configs, New_Configs : Pipe_Configs)
+   is null;
+
+end HW.GFX.GMA.Power_And_Clocks;