gma pipe_setup: Use a plane as cursor in case of y-tiling

Y-tiling allows us to rotate the framebuffer in 90° steps. However,
we'd also want to rotate the cursor. We can use a second (framebuffer)
plane but that has some pitfalls:

* The plane has to be completely within the visible picture, hence we
  can't move the cursor partially out of the picture and have to clip
  it manually.
* Calculating the offsets for the clipping is not easy considering
  the rotation involved. We translate the cursor's position on the
  framebuffer to the final position on the visible picture first,
  then we calculate the offsets. This makes it harder to compare to
  the documented offset calculations.

Because of this, we have to re-write most of the plane registers when
the cursor is clipped. Only when the cursor was and is fully visible,
we can simply "move" its position.

Change-Id: Ibd6258304ef7d9555b279b5e243a8c1aeadbbb93
Signed-off-by: Nico Huber <nico.huber@secunet.com>
Reviewed-on: https://review.sourcearcade.org/c/libgfxinit/+/546
Reviewed-by: Thomas Heijligen <src@posteo.de>
Tested-by: Nico Huber <nico.h@gmx.de>
diff --git a/common/hw-gfx-gma-pipe_setup.adb b/common/hw-gfx-gma-pipe_setup.adb
index c210dd5..f49d6c0 100644
--- a/common/hw-gfx-gma-pipe_setup.adb
+++ b/common/hw-gfx-gma-pipe_setup.adb
@@ -50,6 +50,7 @@
       DSPCNTR_TILED_SURFACE_X_TILED;
 
    PLANE_COLOR_CTL_PLANE_GAMMA_DISABLE : constant := 1 * 2 ** 13;
+   PLANE_COLOR_CTL_ALPHA_MODE_SW_PREMUL: constant := 2 * 2 **  4;
 
    PLANE_CTL_PLANE_ENABLE              : constant := 1 * 2 ** 31;
    PLANE_CTL_SRC_PIX_FMT_RGB_32B_8888  : constant := 4 * 2 ** 24;
@@ -59,6 +60,8 @@
    PLANE_CTL_TILED_SURFACE_X_TILED     : constant := 1 * 2 ** 10;
    PLANE_CTL_TILED_SURFACE_Y_TILED     : constant := 4 * 2 ** 10;
    PLANE_CTL_TILED_SURFACE_YF_TILED    : constant := 5 * 2 ** 10;
+   PLANE_CTL_ALPHA_MODE_MASK           : constant := 3 * 2 **  4;
+   PLANE_CTL_ALPHA_MODE_SW_PREMULTIPLY : constant := 2 * 2 **  4;
 
    PLANE_CTL_TILED_SURFACE : constant array (Tiling_Type) of Word32 :=
      (Linear   => PLANE_CTL_TILED_SURFACE_LINEAR,
@@ -515,30 +518,48 @@
 
    ----------------------------------------------------------------------------
 
+   function Use_Plane_For_Cursor (FB : Framebuffer_Type) return Boolean is
+     (Config.Has_Plane_Control and then FB.Tiling = Y_Tiled);
+
    procedure Update_Cursor
      (Pipe     : Pipe_Index;
       FB       : Framebuffer_Type;
       Cursor   : Cursor_Type)
    is
+      Controller : Controller_Type renames Controllers (Pipe);
    begin
-      -- on some platforms writing CUR_CTL disables self-arming of CUR_POS
-      -- so keep it first
-      Registers.Write
-        (Register => Cursors (Pipe).CTL,
-         Value    => CUR_CTL_MODE (Cursor.Mode, Cursor.Size) or
-                     (if Config.Need_Pipe_Arb_Slots
-                      then MCURSOR_ARB_SLOTS (1)
-                      else CUR_CTL_PIPE_SELECT (Pipe)));
-      Place_Cursor (Pipe, FB, Cursor, (Cursor.Center_X, Cursor.Center_Y));
+      if Use_Plane_For_Cursor (FB) then
+         if Config.Has_Plane_Color_Control then
+            Registers.Write
+              (Register => Controller.PLANE_2_COLOR_CTL,
+               Value    => PLANE_COLOR_CTL_PLANE_GAMMA_DISABLE or
+                           PLANE_COLOR_CTL_ALPHA_MODE_SW_PREMUL);
+         end if;
+      else
+         -- on some platforms writing CUR_CTL disables self-arming of CUR_POS
+         -- so keep it first
+         Registers.Write
+           (Register => Cursors (Pipe).CTL,
+            Value    => CUR_CTL_MODE (Cursor.Mode, Cursor.Size) or
+                        (if Config.Need_Pipe_Arb_Slots
+                         then MCURSOR_ARB_SLOTS (1)
+                         else CUR_CTL_PIPE_SELECT (Pipe)));
+      end if;
+      Place_Cursor (Pipe, FB, Cursor, (Cursor.Center_X, Cursor.Center_Y), Update => True);
    end Update_Cursor;
 
    procedure Place_Cursor
      (Pipe     : Pipe_Index;
       FB       : Framebuffer_Type;
       Cursor   : Cursor_Type;
-      Center   : Cursor_Coord)
+      Center   : Cursor_Coord;
+      Update   : Boolean := False)
    is
+      Controller : Controller_Type renames Controllers (Pipe);
+
       Width : constant Width_Type := Cursor_Width (Cursor.Size);
+      Surface_Width : constant Width_Type := Source_Width (FB);
+      Surface_Height : constant Height_Type := Source_Height (FB);
 
       -- Like `Cursor_Pos`/`Cursor_Coord` but allowing wider range for proof.
       subtype Relaxed_Pos is Int32 range
@@ -570,25 +591,125 @@
         (X => Rotate (Center).X - Width / 2,
          Y => Rotate (Center).Y - Width / 2);
 
+      function Fully_Visible (Origin : Relaxed_Coord) return Boolean is
+        (Origin.X in 0 .. Surface_Width - Width - 1 and
+         Origin.Y in 0 .. Surface_Height - Width - 1);
+
       Origin : Relaxed_Coord := Phys_Origin (Center);
+
+      Visible : constant Boolean :=
+         Origin.X in -Width + 1 .. Surface_Width - 1 and
+         Origin.Y in -Width + 1 .. Surface_Height - 1;
    begin
-      -- off-screen cursor needs special care
-      if Origin.X <= -Width or Origin.Y <= -Width or
-         Origin.X >= Source_Width (FB) or Origin.Y >= Source_Height (FB) or
-         Origin.X > Config.Maximum_Cursor_X or Origin.Y > Config.Maximum_Cursor_Y
+      if Use_Plane_For_Cursor (FB) and then
+         ((Update and Cursor.Mode /= ARGB_Cursor) or not Visible)
       then
-         Origin.X := -Width;
-         Origin.Y := -Width;
+         Registers.Write
+           (Register => Controller.PLANE_2_CTL,
+            Value    => 0,
+            Verbose  => False);
+         Registers.Write   -- arming
+           (Register => Controller.PLANE_2_SURF,
+            Value    => 0,
+            Verbose  => False);
+      elsif Use_Plane_For_Cursor (FB) then
+         -- we may have to configure all registers
+         if Update or
+            not Fully_Visible (Phys_Origin ((Cursor.Center_X, Cursor.Center_Y))) or
+            not Fully_Visible (Origin)
+         then
+            declare
+               Visible_Width  : Width_Type   := Width;
+               Visible_Height : Height_Type  := Width;
+               Offset_X : Word32;
+               Offset_Y : Word32;
+
+               -- Calculates where to start reading from the cursor plane's
+               -- framebuffer, iow. gives the first visible pixel of the
+               -- cursor (e.g. for an unrotated cursor whose upper-left
+               -- corner left the screen).
+               function Offset (Pos : Relaxed_Pos; Visible : Int32) return Word32
+               is
+                 (if (Pos < 0 and FB.Rotation <= Rotated_90) or
+                     (Pos >= 0 and FB.Rotation >= Rotated_180)
+                  then Word32 (Width - Visible) else 0)
+               with
+                  Pre => Visible in 0 .. Width;
+            begin
+               -- The hardware does neither support negative positions nor
+               -- clipping. So if the cursor should be clipped at an edge
+               -- of the framebuffer, we need to make these calculations
+               -- manually:
+
+               if Origin.X < 0 then
+                  Visible_Width := Visible_Width + Origin.X;
+               elsif Origin.X > Surface_Width - Width then
+                  Visible_Width := Surface_Width - Origin.X;
+               end if;
+               Offset_X := Offset (Origin.X, Visible_Width);
+               Origin.X := Int32'Max (Origin.X, 0);
+
+               if Origin.Y < 0 then
+                  Visible_Height := Visible_Height + Origin.Y;
+               elsif Origin.Y > Surface_Height - Width then
+                  Visible_Height := Surface_Height - Origin.Y;
+               end if;
+               Offset_Y := Offset (Origin.Y, Visible_Height);
+               Origin.Y := Int32'Max (Origin.Y, 0);
+
+               Registers.Write
+                 (Register    => Controller.PLANE_2_CTL,
+                  Value       => PLANE_CTL_PLANE_ENABLE or
+                                 PLANE_CTL_SRC_PIX_FMT_RGB_32B_8888 or
+                                 PLANE_CTL_TILED_SURFACE (FB.Tiling) or
+                                 PLANE_CTL_PLANE_ROTATION (FB.Rotation) or
+                                (if not Config.Has_Plane_Color_Control
+                                 then PLANE_CTL_PLANE_GAMMA_DISABLE or
+                                      PLANE_CTL_ALPHA_MODE_SW_PREMULTIPLY
+                                 else 0),
+                  Verbose  => False);
+               Registers.Write
+                 (Register => Controller.PLANE_2_OFFSET,
+                  Value    => Shift_Left (Offset_Y, 16) or Offset_X,
+                  Verbose  => False);
+               Registers.Write
+                 (Register => Controller.PLANE_2_SIZE,
+                  Value    => Encode_Size (Visible_Width, Visible_Height),
+                  Verbose  => False);
+               Registers.Write
+                 (Register => Controller.PLANE_2_STRIDE,
+                  Value    => Word32 (FB_Pitch (Width, FB)),
+                  Verbose  => False);
+            end;
+         end if;
+
+         Registers.Write
+           (Register => Controller.PLANE_2_POS,
+            Value    => Shift_Left (Word32 (Origin.Y), 16) or Word32 (Origin.X),
+            Verbose  => False);
+         Registers.Write   -- arming
+           (Register => Controller.PLANE_2_SURF,
+            Value    => Shift_Left (Word32 (Cursor.GTT_Offset), 12),
+            Verbose  => False);
+      else
+         -- off-screen cursor needs special care
+         if not Visible or
+            Origin.X > Config.Maximum_Cursor_X or
+            Origin.Y > Config.Maximum_Cursor_Y
+         then
+            Origin.X := -Width;
+            Origin.Y := -Width;
+         end if;
+         Registers.Write
+           (Register => Cursors (Pipe).POS,
+            Value    => CUR_POS_Y (Origin.Y) or CUR_POS_X (Origin.X),
+            Verbose  => False);
+         -- write to CUR_BASE always arms other CUR_* registers
+         Registers.Write
+           (Register => Cursors (Pipe).BASE,
+            Value    => Shift_Left (Word32 (Cursor.GTT_Offset), 12),
+            Verbose  => False);
       end if;
-      Registers.Write
-        (Register => Cursors (Pipe).POS,
-         Value    => CUR_POS_Y (Origin.Y) or CUR_POS_X (Origin.X),
-         Verbose  => False);
-      -- write to CUR_BASE always arms other CUR_* registers
-      Registers.Write
-        (Register => Cursors (Pipe).BASE,
-         Value    => Shift_Left (Word32 (Cursor.GTT_Offset), 12),
-         Verbose  => False);
    end Place_Cursor;
 
    ----------------------------------------------------------------------------
diff --git a/common/hw-gfx-gma-pipe_setup.ads b/common/hw-gfx-gma-pipe_setup.ads
index 892d218..3ba0ec4 100644
--- a/common/hw-gfx-gma-pipe_setup.ads
+++ b/common/hw-gfx-gma-pipe_setup.ads
@@ -52,7 +52,8 @@
      (Pipe     : Pipe_Index;
       FB       : Framebuffer_Type;
       Cursor   : Cursor_Type;
-      Center   : Cursor_Coord);
+      Center   : Cursor_Coord;
+      Update   : Boolean := False);
 
    type Scaler_Reservation is private;
    Null_Scaler_Reservation : constant Scaler_Reservation;
diff --git a/gfxtest/hw-gfx-gma-gfx_test.adb b/gfxtest/hw-gfx-gma-gfx_test.adb
index dddb2a8..41d8461 100644
--- a/gfxtest/hw-gfx-gma-gfx_test.adb
+++ b/gfxtest/hw-gfx-gma-gfx_test.adb
@@ -8,6 +8,7 @@
 with HW.PCI.Dev;
 with HW.MMIO_Range;
 with HW.GFX.GMA.Config;
+with HW.GFX.GMA.Registers;
 with HW.GFX.GMA.Display_Probing;
 
 package body HW.GFX.GMA.GFX_Test
@@ -236,12 +237,16 @@
       return Byte (255 - Int32'Min (255, 6 * abs Dist_Circle + 64));
    end Donut;
 
-   procedure Draw_Cursor (Pipe : Pipe_Index; Cursor : Cursor_Type)
+   procedure Draw_Cursor (Pipe : Pipe_Index; Rotation : Rotation_Type; Cursor : Cursor_Type)
    is
       use type HW.Byte;
       Width : constant Width_Type := Cursor_Width (Cursor.Size);
-      Screen_Offset : Natural :=
-         Natural (Shift_Left (Word32 (Cursor.GTT_Offset), 12) / 4);
+      GTT_Offset : constant GTT_Range :=
+        (if Rotation = Rotated_90 or Rotation = Rotated_270 then
+            Cursor.GTT_Offset - GTT_Rotation_Offset
+         else
+            Cursor.GTT_Offset);
+      Screen_Offset : Natural := Natural (Shift_Left (Word32 (GTT_Offset), 12) / 4);
    begin
       if Cursor.Mode /= ARGB_Cursor then
          return;
@@ -309,6 +314,7 @@
 
    procedure Prepare_Cursors
      (Cursors  :    out Cursor_Array;
+      Rotation : in     Rotation_Type;
       Offset   : in out Word32)
    is
       GMA_Phys_Base_Mask : constant := 16#fff0_0000#;
@@ -336,21 +342,69 @@
          Offset := (Offset + Cursor_Align - 1) and not (Cursor_Align - 1);
          declare
             Width : constant Width_Type := Cursor_Width (Size);
-            GTT_End : constant Word32 := Offset + Word32 (Width * Width) * 4;
+            Height : Width_Type renames Width;
+
+            Phys_End : constant Word32 := Offset + Word32 (Width * Height) * 4;
+            GTT_Start : constant GTT_Range := GTT_Range (Shift_Right (Offset, 12));
+            GTT_End   : constant GTT_Range := GTT_Range (Shift_Right (Phys_End, 12));
+            -- 90 degree rotations use a special framebuffer mapping w/ GTT_Rotation_Offset:
+            Scanout_Offset : constant GTT_Range :=
+              (if Rotation in Rotated_90 | Rotated_270 then GTT_Rotation_Offset else 0);
          begin
             Cursors (Size) :=
               (Mode        => ARGB_Cursor,
                Size        => Size,
                Center_X    => Width,
-               Center_Y    => Width,
-               GTT_Offset  => GTT_Range (Shift_Right (Offset, 12)));
-            while Offset < GTT_End loop
+               Center_Y    => Height,
+               GTT_Offset  => GTT_Start + Scanout_Offset);
+
+            while Offset < Phys_End loop
                GMA.Write_GTT
                  (GTT_Page       => GTT_Range (Offset / GTT_Page_Size),
                   Device_Address => GTT_Address_Type (Phys_Base + Offset),
                   Valid          => True);
                Offset := Offset + GTT_Page_Size;
             end loop;
+
+            if Rotation in Rotated_90 | Rotated_270 then
+               -- In case of y-tiled surfaces (needed for 90 degree rotations),
+               -- the fence makes the framebuffer writeable like a linear one.
+               Registers.Add_Fence
+                 (First_Page  => GTT_Start,
+                  Last_Page   => GTT_End - 1,
+                  Tiling      => Y_Tiled,
+                  Pitch       => Natural (Width / Tile_Width (Y_Tiled)),
+                  Success     => Success);
+
+               -- Though, for the scanout of the rotated surface, we have to add
+               -- a special,  rotated framebuffer mapping.  For each linear page
+               -- index we calculate `Phys_Addr` column-wise from bottom to top.
+               declare
+                  subtype Rotated_Pages is GTT_Range range
+                     GTT_Start + GTT_Rotation_Offset .. GTT_End - 1 + GTT_Rotation_Offset;
+
+                  Bytes_Per_Row : constant GTT_Address_Type :=
+                     GTT_Address_Type (Tile_Rows (Y_Tiled) * Width * 4);
+                  V_Pages : constant GTT_Range := GTT_Range (Height / Tile_Rows (Y_Tiled));
+                  V_Bytes : constant GTT_Address_Type :=
+                     GTT_Address_Type (V_Pages) * Bytes_Per_Row;
+
+                  Phys_Addr : GTT_Address_Type := GTT_Address_Type (Phys_Base + Phys_End);
+               begin
+                  for Page in Rotated_Pages loop
+                     Phys_Addr := Phys_Addr - Bytes_Per_Row;
+
+                     Registers.Write_GTT
+                       (GTT_Page       => Page,
+                        Device_Address => Phys_Addr,
+                        Valid          => True);
+
+                     if (Page - Rotated_Pages'First + 1) mod V_Pages = 0 then
+                        Phys_Addr := Phys_Addr + GTT_Page_Size + V_Bytes;
+                     end if;
+                  end loop;
+               end;
+            end if;
          end;
       end loop;
    end Prepare_Cursors;
@@ -381,7 +435,7 @@
                Pipes (Pipe).Port := GMA.Disabled;
             end if;
          end if;
-         Prepare_Cursors (Cursors (Pipe), Offset);
+         Prepare_Cursors (Cursors (Pipe), Rotation, Offset);
          Pipes (Pipe).Cursor := Cursors (Pipe) (Cursor_Size'Val (Rand (Gen) mod 3));
       end loop;
 
@@ -565,7 +619,10 @@
       end loop;
    end Move_Cursors;
 
-   procedure Run_The_Show (Deadline : Time.T; Gen : Rand_P.Generator)
+   procedure Run_The_Show
+     (Deadline : Time.T;
+      Gen      : Rand_P.Generator;
+      Rotation : Rotation_Type)
    is
       Timed_Out : Boolean;
       Hotplug_List : GMA.Display_Probing.Port_List;
@@ -586,7 +643,7 @@
                Pipe        => Pipe);
          end if;
          for Size in Cursor_Size loop
-            Draw_Cursor (Pipe, Cursors (Pipe) (Size));
+            Draw_Cursor (Pipe, Rotation, Cursors (Pipe) (Size));
          end loop;
       end loop;
 
@@ -740,7 +797,7 @@
                   end if;
                end loop;
 
-               Run_The_Show (Deadline, Gen);
+               Run_The_Show (Deadline, Gen, Rotation);
 
                for Pipe in GMA.Pipe_Index loop
                   if Pipes (Pipe).Port /= GMA.Disabled then