using System;
using System.Collections.Generic;
using AcDream.App.UI;
using Silk.NET.Core;
using Silk.NET.GLFW;
using Silk.NET.Input;
namespace AcDream.App.Rendering;
///
/// Process-lifetime cache of native GLFW cursors, keyed by cursor media.
///
///
/// #348: Silk's per-mouse ICursor recreates the native Win32 cursor
/// on every Image assignment. A per-frame cursor alternation — the
/// pick cursor flickering between two kinds while hovering an animated
/// NPC — therefore allocates a fresh USER-object handle on every flip
/// until Win32 CreateCursor fails with "Not enough memory" and the
/// render loop dies (exit 82, vendor-gate.log 2026-08-08). Retail loads
/// each MediaDescCursor once and switches between loaded cursors; this
/// cache restores that shape: one glfwCreateCursor per distinct
/// media for the process lifetime, and an O(1), zero-allocation
/// glfwSetCursor per switch.
///
///
internal sealed unsafe class GlfwCursorCache : IDisposable
{
private readonly Glfw _glfw;
private readonly WindowHandle* _window;
private readonly Dictionary _customCursors = new();
private readonly Dictionary _standardCursors = new();
private bool _disposed;
private GlfwCursorCache(Glfw glfw, WindowHandle* window)
{
_glfw = glfw;
_window = window;
}
///
/// Returns null when no native GLFW window is available (tests,
/// non-GLFW platforms) — callers then stay on the Silk path.
///
public static GlfwCursorCache? TryCreate(nint glfwWindowHandle)
=> glfwWindowHandle == 0
? null
: new GlfwCursorCache(Glfw.GetApi(), (WindowHandle*)glfwWindowHandle);
///
/// Sets the window cursor, creating the native cursor only on the
/// first use of this media. A media GLFW rejects is cached as a
/// permanent miss so the failure cannot re-trigger per frame.
///
public bool TrySetCustom(UiCursorMedia media, RawImage image)
{
if (_disposed)
return false;
if (!_customCursors.TryGetValue(media, out nint cursor))
{
cursor = CreateCustomCursor(media, image);
_customCursors[media] = cursor;
}
if (cursor == 0)
return false;
_glfw.SetCursor(_window, (Cursor*)cursor);
return true;
}
///
/// Sets a standard OS cursor through the same cached-native route.
/// Arrow uses GLFW's default-cursor restore; the other shapes cover
/// the AP-72 missing-art fallback set. Unknown shapes report false so
/// the caller can keep its Silk fallback.
///
public bool TrySetStandard(StandardCursor desired)
{
if (_disposed)
return false;
if (desired == StandardCursor.Arrow)
{
_glfw.SetCursor(_window, null);
return true;
}
CursorShape shape;
switch (desired)
{
case StandardCursor.Hand: shape = CursorShape.Hand; break;
case StandardCursor.Crosshair: shape = CursorShape.Crosshair; break;
case StandardCursor.IBeam: shape = CursorShape.IBeam; break;
default: return false;
}
if (!_standardCursors.TryGetValue(desired, out nint cursor))
{
cursor = (nint)_glfw.CreateStandardCursor(shape);
_standardCursors[desired] = cursor;
}
if (cursor == 0)
return false;
_glfw.SetCursor(_window, (Cursor*)cursor);
return true;
}
private nint CreateCustomCursor(UiCursorMedia media, RawImage image)
{
// GLFW copies the pixel data before CreateCursor returns, so the
// pin only needs to span the call.
fixed (byte* pixels = image.Pixels.Span)
{
var glfwImage = new Image
{
Width = image.Width,
Height = image.Height,
Pixels = pixels,
};
return (nint)_glfw.CreateCursor(&glfwImage, media.HotspotX, media.HotspotY);
}
}
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_glfw.SetCursor(_window, null);
foreach (nint cursor in _customCursors.Values)
{
if (cursor != 0)
_glfw.DestroyCursor((Cursor*)cursor);
}
foreach (nint cursor in _standardCursors.Values)
{
if (cursor != 0)
_glfw.DestroyCursor((Cursor*)cursor);
}
_customCursors.Clear();
_standardCursors.Clear();
}
}