fix(app): #348 — cursor switches ride a process-lifetime native cache; the per-flip Win32 handle leak is closed
Silk's per-mouse ICursor recreates the native Win32 cursor on every Image assignment; a per-frame cursor alternation (the pick cursor flickering between kinds while hovering an ANIMATED NPC — exactly the stand-at-a-vendor posture) allocated a fresh USER handle each flip until CreateCursor died with "Not enough memory" and took the render loop with it (vendor-gate.log, exit 82 — surfaced as one clean stack by #343's fix, as designed). GlfwCursorCache restores retail's own shape: each distinct MediaDescCursor is created ONCE for the process lifetime (glfwCreateCursor, rejected media cached as permanent misses) and switching is an O(1) zero-allocation glfwSetCursor. The AP-72 missing-art standard-cursor fallback rides the same cache (Arrow/Hand/Crosshair/IBeam; anything else keeps the Silk path). Graphical hosts attach after the native window exists; tests and windowless hosts keep the Silk path untouched. RetailCursorManager's dedup and PlanApplication logic are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
c721830e71
commit
9d3df5f627
5 changed files with 245 additions and 1 deletions
|
|
@ -461,6 +461,16 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Cursor.Manager.Apply(mice, feedback);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// #348: opts cursor application into the process-lifetime native
|
||||
/// GLFW cursor cache. Called once by the graphical host after the
|
||||
/// native window exists; zero handle keeps the Silk fallback.
|
||||
/// </summary>
|
||||
public void AttachNativeCursorWindow(nint glfwWindowHandle)
|
||||
{
|
||||
_bindings.Cursor.Manager.AttachNativeWindow(glfwWindowHandle);
|
||||
}
|
||||
|
||||
public void RestoreLayout() => _persistence?.RestoreAll();
|
||||
|
||||
public void SaveLayout() => _persistence?.SaveAll();
|
||||
|
|
@ -1919,6 +1929,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
private void MountVendor()
|
||||
{
|
||||
ImportedLayout? layout;
|
||||
uint emptySlotSprite;
|
||||
lock (_bindings.Assets.DatLock)
|
||||
{
|
||||
layout = LayoutImporter.Import(
|
||||
|
|
@ -1928,6 +1939,13 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
_bindings.Assets.ResolveSprite,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.ResolveFont);
|
||||
// F7b (Slice 5.4 review): the authored empty-slot background for
|
||||
// the item strip, same resolution path ExternalContainerController
|
||||
// uses for its own lists.
|
||||
emptySlotSprite = ItemListCellTemplate.ResolveEmptySprite(
|
||||
_bindings.Assets.Dats,
|
||||
VendorUiController.LayoutId,
|
||||
VendorUiController.ItemListId);
|
||||
}
|
||||
if (layout is null)
|
||||
{
|
||||
|
|
@ -1963,7 +1981,23 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
});
|
||||
|
||||
VendorRuntimeBindings b = _bindings.Vendor;
|
||||
VendorController = VendorUiController.Bind(layout, b.State, handle, b.ResolveIcon);
|
||||
// F1/F2/F3/F7b (Slice 5.4 review): the category dropdown needs
|
||||
// fonts/sprites to draw at all; the price text needs the local
|
||||
// player's coin total, sourced from the SAME ClientObjectTable/
|
||||
// PlayerGuid pair InventoryRuntimeBindings already exposes (no new
|
||||
// binding record needed — this mirrors how MountExternalContainer
|
||||
// reads its own sibling binding).
|
||||
VendorController = VendorUiController.Bind(
|
||||
layout,
|
||||
b.State,
|
||||
handle,
|
||||
b.ResolveIcon,
|
||||
_bindings.Inventory.Objects,
|
||||
_bindings.Inventory.PlayerGuid,
|
||||
_bindings.Assets.DefaultFont,
|
||||
_bindings.Assets.DebugFont,
|
||||
_bindings.Assets.ResolveSprite,
|
||||
emptySlotSprite);
|
||||
if (VendorController is null)
|
||||
{
|
||||
Console.WriteLine("[M4] vendor: required authored controls are missing.");
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue