Provide an explicit size of 512, smaller than the default 2048. The cursors for all mods still pack onto this single sheet, so we avoid wasting memory on larger sheets. Sorting the cursors also helps pack them onto the sheets more efficiently.
300 lines
8.2 KiB
C#
300 lines
8.2 KiB
C#
#region Copyright & License Information
|
|
/*
|
|
* Copyright (c) The OpenRA Developers and Contributors
|
|
* This file is part of OpenRA, which is free software. It is made
|
|
* available to you under the terms of the GNU General Public License
|
|
* as published by the Free Software Foundation, either version 3 of
|
|
* the License, or (at your option) any later version. For more
|
|
* information, see COPYING.
|
|
*/
|
|
#endregion
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using OpenRA.Primitives;
|
|
|
|
namespace OpenRA.Graphics
|
|
{
|
|
public sealed class CursorManager
|
|
{
|
|
sealed class Cursor
|
|
{
|
|
public string Name;
|
|
public int2 PaddedSize;
|
|
public Rectangle Bounds;
|
|
|
|
public int Length;
|
|
public Sprite[] Sprites;
|
|
public IHardwareCursor[] Cursors;
|
|
}
|
|
|
|
readonly Dictionary<string, Cursor> cursors = new();
|
|
readonly SheetBuilder sheetBuilder;
|
|
readonly GraphicSettings graphicSettings;
|
|
|
|
Cursor cursor;
|
|
bool isLocked = false;
|
|
int2 lockedPosition;
|
|
readonly bool hardwareCursorsDisabled = false;
|
|
bool hardwareCursorsDoubled = false;
|
|
|
|
public CursorManager(CursorProvider cursorProvider)
|
|
{
|
|
hardwareCursorsDisabled = Game.Settings.Graphics.DisableHardwareCursors;
|
|
|
|
graphicSettings = Game.Settings.Graphics;
|
|
sheetBuilder = new SheetBuilder(SheetType.BGRA, 512);
|
|
|
|
// Sort the cursors for better packing onto the sheet.
|
|
foreach (var kv in cursorProvider.Cursors
|
|
.OrderBy(kvp => kvp.Value.Frames.Max(f => f.Size.Height)))
|
|
{
|
|
var frames = kv.Value.Frames;
|
|
var palette = !string.IsNullOrEmpty(kv.Value.Palette) ? cursorProvider.Palettes[kv.Value.Palette] : null;
|
|
|
|
var c = new Cursor
|
|
{
|
|
Name = kv.Key,
|
|
Bounds = Rectangle.FromLTRB(0, 0, 1, 1),
|
|
|
|
Length = 0,
|
|
Sprites = new Sprite[frames.Length],
|
|
Cursors = new IHardwareCursor[frames.Length]
|
|
};
|
|
|
|
// Hardware cursors have a number of odd platform-specific bugs/limitations.
|
|
// Reduce the number of edge cases by padding the individual frames such that:
|
|
// - the hotspot is inside the frame bounds (enforced by SDL)
|
|
// - all frames within a sequence have the same size (needed for macOS 10.15)
|
|
// - the frame size is a multiple of 8 (needed for Windows)
|
|
foreach (var f in frames)
|
|
{
|
|
// Hotspot is specified relative to the center of the frame
|
|
var hotspot = f.Offset.ToInt2() - kv.Value.Hotspot - new int2(f.Size) / 2;
|
|
|
|
// Resolve indexed data to real colours
|
|
var data = f.Data;
|
|
var type = f.Type;
|
|
if (type == SpriteFrameType.Indexed8)
|
|
{
|
|
data = ConvertIndexedToBgra(kv.Key, f, palette);
|
|
type = SpriteFrameType.Bgra32;
|
|
}
|
|
|
|
c.Sprites[c.Length++] = sheetBuilder.Add(data, type, f.Size, 0, hotspot);
|
|
|
|
// Bounds relative to the hotspot
|
|
c.Bounds = Rectangle.Union(c.Bounds, new Rectangle(hotspot, f.Size));
|
|
}
|
|
|
|
// Pad bottom-right edge to make the frame size a multiple of 8
|
|
c.PaddedSize = 8 * new int2((c.Bounds.Width + 7) / 8, (c.Bounds.Height + 7) / 8);
|
|
|
|
cursors.Add(kv.Key, c);
|
|
}
|
|
|
|
CreateOrUpdateHardwareCursors();
|
|
Update();
|
|
}
|
|
|
|
void CreateOrUpdateHardwareCursors()
|
|
{
|
|
if (hardwareCursorsDisabled)
|
|
return;
|
|
|
|
// Dispose any existing cursors to avoid leaking native resources
|
|
ClearHardwareCursors();
|
|
|
|
foreach (var kv in cursors)
|
|
{
|
|
var template = kv.Value;
|
|
for (var i = 0; i < template.Sprites.Length; i++)
|
|
{
|
|
template.Cursors[i]?.Dispose();
|
|
|
|
// Calculate the padding to position the frame within sequenceBounds
|
|
var paddingTL = -(template.Bounds.Location - template.Sprites[i].Offset.XY.ToInt2());
|
|
var paddingBR = template.PaddedSize - new int2(template.Sprites[i].Bounds.Size) - paddingTL;
|
|
|
|
var hardwareCursor = CreateHardwareCursor(kv.Key, template.Sprites[i], paddingTL, paddingBR, -template.Bounds.Location);
|
|
if (hardwareCursor != null)
|
|
template.Cursors[i] = hardwareCursor;
|
|
else
|
|
{
|
|
Log.Write("debug", $"Failed to initialize hardware cursor for {template.Name}.");
|
|
Console.WriteLine($"Failed to initialize hardware cursor for {template.Name}.");
|
|
}
|
|
}
|
|
}
|
|
|
|
sheetBuilder.Current.ReleaseBuffer();
|
|
|
|
hardwareCursorsDoubled = graphicSettings.CursorDouble;
|
|
}
|
|
|
|
public void SetCursor(string cursorName)
|
|
{
|
|
if ((cursorName == null && cursor == null) || (cursor != null && cursorName == cursor.Name))
|
|
return;
|
|
|
|
if (cursorName == null || !cursors.TryGetValue(cursorName, out cursor))
|
|
cursor = null;
|
|
|
|
Update();
|
|
}
|
|
|
|
int frame;
|
|
int ticks;
|
|
|
|
public void Tick()
|
|
{
|
|
if (hardwareCursorsDoubled != graphicSettings.CursorDouble)
|
|
{
|
|
CreateOrUpdateHardwareCursors();
|
|
Update();
|
|
}
|
|
|
|
if (cursor == null || cursor.Cursors.Length == 1)
|
|
return;
|
|
|
|
if (++ticks > 2)
|
|
{
|
|
ticks -= 2;
|
|
frame++;
|
|
|
|
Update();
|
|
}
|
|
}
|
|
|
|
void Update()
|
|
{
|
|
if (cursor != null && frame >= cursor.Cursors.Length)
|
|
frame %= cursor.Cursors.Length;
|
|
|
|
var hardwareCursor = cursor?.Cursors[frame];
|
|
if (hardwareCursor == null || isLocked)
|
|
Game.Renderer.Window.SetHardwareCursor(null);
|
|
else
|
|
Game.Renderer.Window.SetHardwareCursor(hardwareCursor);
|
|
}
|
|
|
|
public void Render(Renderer renderer)
|
|
{
|
|
// Cursor is hidden
|
|
if (cursor == null)
|
|
return;
|
|
|
|
// Hardware cursor is enabled
|
|
if (!isLocked && cursor.Cursors[frame % cursor.Length] != null)
|
|
return;
|
|
|
|
// Render cursor in software
|
|
var doubleCursor = graphicSettings.CursorDouble;
|
|
var cursorSprite = cursor.Sprites[frame % cursor.Length];
|
|
var cursorScale = doubleCursor ? 2 : 1;
|
|
|
|
// Cursor is rendered in native window coordinates
|
|
// Apply same scaling rules as hardware cursors
|
|
if (Game.Renderer.NativeWindowScale > 1.5f)
|
|
cursorScale *= 2;
|
|
|
|
var mousePos = isLocked ? lockedPosition : Viewport.LastMousePos;
|
|
renderer.RgbaSpriteRenderer.DrawSprite(cursorSprite,
|
|
mousePos,
|
|
cursorScale / Game.Renderer.WindowScale);
|
|
}
|
|
|
|
public void Lock()
|
|
{
|
|
lockedPosition = Viewport.LastMousePos;
|
|
Game.Renderer.Window.SetRelativeMouseMode(true);
|
|
isLocked = true;
|
|
Update();
|
|
}
|
|
|
|
public void Unlock()
|
|
{
|
|
Game.Renderer.Window.SetRelativeMouseMode(false);
|
|
isLocked = false;
|
|
Update();
|
|
}
|
|
|
|
public static byte[] ConvertIndexedToBgra(string name, ISpriteFrame frame, ImmutablePalette palette)
|
|
{
|
|
if (frame.Type != SpriteFrameType.Indexed8)
|
|
throw new ArgumentException("ConvertIndexedToBgra requires input frames to be indexed.", nameof(frame));
|
|
|
|
// All palettes must be explicitly referenced, even if they are embedded in the sprite.
|
|
if (palette == null)
|
|
throw new InvalidOperationException($"Cursor sequence `{name}` attempted to load an indexed sprite but does not define Palette");
|
|
|
|
var width = frame.Size.Width;
|
|
var height = frame.Size.Height;
|
|
|
|
if (width == 0 || height == 0)
|
|
return Array.Empty<byte>();
|
|
|
|
var data = new byte[4 * width * height];
|
|
unsafe
|
|
{
|
|
// Cast the data to an int array so we can copy the src data directly
|
|
fixed (byte* bd = &data[0])
|
|
{
|
|
var rgba = (uint*)bd;
|
|
for (var j = 0; j < height; j++)
|
|
for (var i = 0; i < width; i++)
|
|
rgba[j * width + i] = palette[frame.Data[j * width + i]];
|
|
}
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
IHardwareCursor CreateHardwareCursor(string name, Sprite data, int2 paddingTL, int2 paddingBR, int2 hotspot)
|
|
{
|
|
var size = data.Bounds.Size;
|
|
var srcStride = data.Sheet.Size.Width;
|
|
var srcData = data.Sheet.GetData();
|
|
var newWidth = paddingTL.X + size.Width + paddingBR.X;
|
|
var newHeight = paddingTL.Y + size.Height + paddingBR.Y;
|
|
var rgbaData = new byte[4 * newWidth * newHeight];
|
|
|
|
for (var j = 0; j < size.Height; j++)
|
|
{
|
|
for (var i = 0; i < size.Width; i++)
|
|
{
|
|
var src = 4 * ((j + data.Bounds.Top) * srcStride + data.Bounds.Left + i);
|
|
var dest = 4 * ((j + paddingTL.Y) * newWidth + i + paddingTL.X);
|
|
Array.Copy(srcData, src, rgbaData, dest, 4);
|
|
}
|
|
}
|
|
|
|
return Game.Renderer.Window.CreateHardwareCursor(name, new Size(newWidth, newHeight), rgbaData, hotspot, graphicSettings.CursorDouble);
|
|
}
|
|
|
|
void ClearHardwareCursors()
|
|
{
|
|
foreach (var c in cursors.Values)
|
|
{
|
|
for (var i = 0; i < c.Cursors.Length; i++)
|
|
{
|
|
if (c.Cursors[i] != null)
|
|
{
|
|
c.Cursors[i].Dispose();
|
|
c.Cursors[i] = null;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
ClearHardwareCursors();
|
|
|
|
cursors.Clear();
|
|
sheetBuilder.Dispose();
|
|
}
|
|
}
|
|
}
|