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/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;