diff --git a/UIXrender/Engine/BufferReceivedHandler.cs b/UIXrender/Engine/BufferReceivedHandler.cs deleted file mode 100644 index 07ec3bb..0000000 --- a/UIXrender/Engine/BufferReceivedHandler.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Engine; - -// A custom (non-generic) delegate so ReadOnlySpan is legal here -- Span can't -// be an Action/Func type argument (ref structs aren't valid generic arguments), -// but a hand-declared delegate type can take one directly. -public delegate void BufferReceivedHandler(ContextID sourceContext, RENDERHANDLE bufferHandle, BufferFlags flags, ReadOnlySpan data); diff --git a/UIXrender/Engine/ContextRegistry.cs b/UIXrender/Engine/ContextRegistry.cs deleted file mode 100644 index 543c14d..0000000 --- a/UIXrender/Engine/ContextRegistry.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Collections.Concurrent; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Engine; - -// Maps a registered ContextID to the handler it should receive deliveries on via -// SendBuffer -- the smallest coherent slice of "Engine core" (see -// logs/UIXrender/EngineCore.md) needed for StartRenderThread and SendBuffer to mean -// anything together. Holds an ordinary delegate now (not a raw callback pointer) -- -// pointer marshaling, where it's still needed for native callers, lives entirely in -// Interop/EngineApi.cs. -internal static class ContextRegistry -{ - private static readonly ConcurrentDictionary s_contexts = new(); - - public static void Register(ContextID id, BufferReceivedHandler handler) => s_contexts[id.value] = handler; - - public static void Unregister(ContextID id) => s_contexts.TryRemove(id.value, out _); - - public static bool TryGet(ContextID id, out BufferReceivedHandler handler) => s_contexts.TryGetValue(id.value, out handler); -} diff --git a/UIXrender/Engine/EngineService.cs b/UIXrender/Engine/EngineService.cs deleted file mode 100644 index 8c3c5af..0000000 --- a/UIXrender/Engine/EngineService.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Protocol; -using Microsoft.Iris.Render.Subsystems.Os; -using Microsoft.Iris.Render.Subsystems.Remote; - -namespace Microsoft.Iris.Render.Engine; - -// The idiomatic managed API for UIXrender's core transport -- the single thing both -// Interop/EngineApi.cs's [UnmanagedCallersOnly] shims (for native callers) and managed -// callers like UIX.RenderApi call into, so there is exactly one implementation of each -// operation. The buffer-delivery path (StartRenderThread/SendBuffer) is entirely -// pointer-free. The remainder (Invoke, the SpRemote* family, ObjectRelease) deal in -// opaque IntPtr *handles* the caller round-trips back to us -- never dereferenced here -- -// plus, for Invoke only, a raw function pointer the caller asked us to run; that one spot -// is the sole `unsafe` in this type. See logs/UIXrender/EngineCore.md and FullSurface.md. -public static class EngineService -{ - // ---- buffer delivery (pointer-free) ---------------------------------------------- - - public static IRenderThreadHandle StartRenderThread(ContextID contextId, BufferReceivedHandler onBufferReceived) - => RenderThread.Start(contextId, onBufferReceived); - - public static HRESULT SendBuffer(ContextID sourceContext, ContextID destContext, RENDERHANDLE bufferHandle, BufferFlags flags, ReadOnlySpan data) - { - if (!ContextRegistry.TryGet(destContext, out BufferReceivedHandler handler)) - return HRESULT.E_FAIL; - - handler(sourceContext, bufferHandle, flags, data); - return HRESULT.S_OK; - } - - // ---- message pump ---------------------------------------------------------------- - - // Drains the render thread's message queue (running any deferred work posted to it) and - // reports WorkResult flags: ProcessedMessage (1) if work ran, else None (0). Never - // NewUserMessage (2) -- that flag drives Win32 TranslateMessage/DispatchMessage, which - // needs a real HWND this backend-agnostic pump doesn't have; leaving it unset routes - // ProcessNativeEvents down its non-Win32 branch. See logs/UIXrender/Rendering.md. - public static uint PeekMessage(uint filterMin, uint filterMax, uint removeMsg) => - MessagePump.Peek() ? 1u : 0u; - - // Blocks the render thread until work is posted (e.g. InterThreadWake) or the timeout - // elapses -- a real wait that returns early on a wake, not a fixed sleep. - public static void WaitMessage(uint timeoutMs) => MessagePump.Wait(timeoutMs); - - // A null function pointer is InterThreadWake: post a wake so a blocked WaitMessage - // returns. A real pointer is a deferred invoke -- run inline when synchronous, else - // queue it to run on the pump (render) thread the next time it peeks, rather than on a - // random threadpool thread. - public static HRESULT Invoke(ContextID context, IntPtr pfnInvoke, IntPtr pvArgs, bool synchronous) - { - if (pfnInvoke == IntPtr.Zero) - { - MessagePump.PostWake(); - return HRESULT.S_OK; - } - - if (synchronous) - { - RunInvoke(pfnInvoke, pvArgs); - } - else - { - IntPtr fn = pfnInvoke; - IntPtr args = pvArgs; - MessagePump.Post(() => RunInvoke(fn, args)); - } - return HRESULT.S_OK; - } - - private static unsafe void RunInvoke(IntPtr fn, IntPtr args) => - ((delegate* unmanaged)fn)(args); - - // Registers a windowing backend that feeds OS window/input messages into the pump. - // UIXrender core never creates a window itself (that would require a concrete GLFW/SDL - // backend, breaking backend-agnosticism); a host or optional backend package supplies - // one through this seam. See logs/UIXrender/Rendering.md. - public static void SetWindowMessageSource(IWindowMessageSource source) => - MessagePump.SetWindowSource(source); - - // ---- remote channel (opaque IntPtr handles, never dereferenced) ------------------- - - public static HRESULT RemoteCreateServerStreams(string session, TransportProtocol protocol, out IntPtr sendStream, out IntPtr receiveStream) - => RemoteServerConnection.CreateServerStreams(protocol, session, out sendStream, out receiveStream); - - public static HRESULT RemoteWaitServerStreamsConnected(TransportProtocol protocol, IntPtr sendStream) - => RemoteServerConnection.WaitConnected(protocol, sendStream); - - public static HRESULT RemoteServerInit(IntPtr sendStream, ContextID context, BufferReceivedHandler onRemoteToLocal, out IntPtr session) - => RemoteServerConnection.ServerInit(sendStream, context, onRemoteToLocal, out session); - - public static HRESULT RemoteServerUninit(IntPtr session, bool forceShutdown, out ShutdownReason reason) - => RemoteServerConnection.ServerUninit(session, forceShutdown, out reason); - - // Backs SpObjectRelease in the managed-direct path, whose only callers release the - // remote stream handles minted by RemoteCreateServerStreams. - public static void ReleaseRemoteStream(IntPtr streamHandle) - => RemoteServerConnection.ReleaseHandle(streamHandle); - - // ---- effects --------------------------------------------------------------------- - - // HLSL effect compilation is a graphics-API-specific service with no cross-platform - // abstraction available under this project's dependency policy -- so this reports - // "not implemented" honestly rather than faking a compiled blob. See - // logs/UIXrender/FullSurface.md, decision 1. - public static HRESULT Dx9CompileEffect() => HRESULT.E_NOTIMPL; -} diff --git a/UIXrender/Engine/HandleTable.cs b/UIXrender/Engine/HandleTable.cs deleted file mode 100644 index 1f6b122..0000000 --- a/UIXrender/Engine/HandleTable.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Engine; - -// Every UIXrender export that hands an opaque "pointer" back to a caller (schema objects, -// XML readers, rich-text objects, bitmaps, data queries, ...) uses this: the managed -// object is pinned by a GCHandle and the caller only ever sees the GCHandle's IntPtr. -// This is the same convention SpRenderThreadInit already established for its thread -// handle -- centralised here so ~10 subsystems don't each re-derive it. -// -// Deliberately not a "real" pointer: the original native library handed out genuine C++ -// object pointers, but nothing outside UIXrender is allowed to dereference these (the -// managed declarations all type them as opaque IntPtr/HANDLE), so an indirection is -// both safe and much harder to corrupt. -internal static class HandleTable -{ - public static IntPtr Alloc(object value) => - value == null ? IntPtr.Zero : GCHandle.ToIntPtr(GCHandle.Alloc(value, GCHandleType.Normal)); - - public static T Get(IntPtr handle) where T : class => - handle == IntPtr.Zero ? null : GCHandle.FromIntPtr(handle).Target as T; - - public static bool TryGet(IntPtr handle, out T value) where T : class - { - value = Get(handle); - return value != null; - } - - public static void Free(IntPtr handle) - { - if (handle == IntPtr.Zero) - return; - - GCHandle gc = GCHandle.FromIntPtr(handle); - if (gc.IsAllocated) - { - (gc.Target as IDisposable)?.Dispose(); - gc.Free(); - } - } -} diff --git a/UIXrender/Engine/IRenderThreadHandle.cs b/UIXrender/Engine/IRenderThreadHandle.cs deleted file mode 100644 index f42ff2f..0000000 --- a/UIXrender/Engine/IRenderThreadHandle.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Engine; - -public interface IRenderThreadHandle : IDisposable -{ - ContextID ContextId { get; } -} diff --git a/UIXrender/Engine/RenderThread.cs b/UIXrender/Engine/RenderThread.cs deleted file mode 100644 index 435d189..0000000 --- a/UIXrender/Engine/RenderThread.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System; -using System.Threading; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Engine; - -// Backs EngineService.StartRenderThread: spins up a genuine OS thread the CLR didn't -// create, which invokes the registered handler once (with synthetic data, proving the -// round trip) and then waits for shutdown -- this is the exact mechanism every -// Remote*/Local*Callback class in UIX.RenderApi depends on via LocalChannel.Connect(). -// See logs/UIXrender/EngineCore.md. Not yet the full message-pump loop (that's the -// larger "Engine core" subsystem, still ahead) -- this proves the interop mechanism, -// not the whole dispatch engine. No pointers here at all now -- callers (native or -// managed) each supply an ordinary BufferReceivedHandler delegate. -internal sealed class RenderThread : IRenderThreadHandle -{ - private readonly BufferReceivedHandler _handler; - private readonly ManualResetEventSlim _shutdown = new(false); - private readonly Thread _thread; - - public ContextID ContextId { get; } - - private RenderThread(ContextID contextId, BufferReceivedHandler handler) - { - ContextId = contextId; - _handler = handler; - _thread = new Thread(Run) { IsBackground = true, Name = $"UIXrender.RenderThread[{contextId.value}]" }; - } - - public static RenderThread Start(ContextID contextId, BufferReceivedHandler handler) - { - var thread = new RenderThread(contextId, handler); - ContextRegistry.Register(contextId, handler); - thread._thread.Start(); - return thread; - } - - private void Run() - { - _handler?.Invoke(ContextId, RENDERHANDLE.NULL, default, ReadOnlySpan.Empty); - _shutdown.Wait(); - } - - public void Dispose() - { - ContextRegistry.Unregister(ContextId); - _shutdown.Set(); - _thread.Join(); - _shutdown.Dispose(); - } -} diff --git a/UIXrender/Interop/BufferInfo.cs b/UIXrender/Interop/BufferInfo.cs deleted file mode 100644 index cbbc9b1..0000000 --- a/UIXrender/Interop/BufferInfo.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of EngineApi.BufferFlags/EngineApi.BufferInfo -// (UIX.RenderApi/Microsoft/Iris/Render/Protocol/EngineApi.cs). -[Flags] -public enum BufferFlags -{ - IsBatch = 1, - CopyData = 2, - Valid = CopyData | IsBatch, -} - -[StructLayout(LayoutKind.Sequential)] -public struct BufferInfo -{ - public ContextID idContextSrc; - public ContextID idContextDest; - public RENDERHANDLE idBuffer; - public BufferFlags nFlags; - public uint cbSizeBuffer; -} diff --git a/UIXrender/Interop/Com/ComVtable.cs b/UIXrender/Interop/Com/ComVtable.cs deleted file mode 100644 index c26625f..0000000 --- a/UIXrender/Interop/Com/ComVtable.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; - -namespace Microsoft.Iris.Render.Interop.Com; - -// Several UIXrender exports take a COM callback interface (IUIXListCallbacks, -// IRawUIXServices, IRichTextCallbacks, IImeCallbacks). Their managed declarations use -// [MarshalAs(UnmanagedType.Interface)], but an [UnmanagedCallersOnly] method cannot -// accept a non-blittable parameter -- what actually arrives on the wire either way is a -// pointer to the object's vtable-bearing COM instance. So UIXrender receives IntPtr and -// calls through the vtable here. -// -// This is the "reconstruct the interface rather than invent a raw pointer type" approach -// CLAUDE.md's *COM objects* section prescribes, applied in the callee direction: slot -// numbering starts at 3 because every one of these interfaces is -// ComInterfaceType.InterfaceIsIUnknown, so slots 0/1/2 are -// QueryInterface/AddRef/Release, and the declared methods follow in declaration order. -internal static unsafe class ComVtable -{ - // Slot 0/1/2 of IUnknown. - public const int SlotQueryInterface = 0; - public const int SlotAddRef = 1; - public const int SlotRelease = 2; - - // First slot available to a derived interface's own methods. - public const int FirstMethodSlot = 3; - - public static void* Slot(IntPtr comObject, int slot) - { - if (comObject == IntPtr.Zero) - return null; - - void** vtable = *(void***)comObject; - return vtable[slot]; - } - - public static uint AddRef(IntPtr comObject) - { - void* fn = Slot(comObject, SlotAddRef); - return fn == null ? 0 : ((delegate* unmanaged)fn)(comObject); - } - - public static uint Release(IntPtr comObject) - { - void* fn = Slot(comObject, SlotRelease); - return fn == null ? 0 : ((delegate* unmanaged)fn)(comObject); - } -} diff --git a/UIXrender/Interop/ContextID.cs b/UIXrender/Interop/ContextID.cs deleted file mode 100644 index f281d08..0000000 --- a/UIXrender/Interop/ContextID.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Protocol.ContextID -// (UIX.RenderApi/Microsoft/Iris/Render/Protocol/ContextID.cs). -[StructLayout(LayoutKind.Sequential)] -public struct ContextID -{ - public static readonly ContextID NULL = new(0); - public static readonly ContextID CURRENT = new(uint.MaxValue); - - public uint value; - - public ContextID(uint value) => this.value = value; - - public static bool operator ==(ContextID a, ContextID b) => a.value == b.value; - public static bool operator !=(ContextID a, ContextID b) => a.value != b.value; - - public override bool Equals(object obj) => obj is ContextID other && value == other.value; - public override int GetHashCode() => (int)value; -} diff --git a/UIXrender/Interop/Drawing/Color.cs b/UIXrender/Interop/Drawing/Color.cs deleted file mode 100644 index 044508c..0000000 --- a/UIXrender/Interop/Drawing/Color.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.Drawing.Color (UIX/Microsoft/Iris/Drawing/Color.cs) -// -- a single packed 0xAARRGGBB uint, not four separate byte fields. -[StructLayout(LayoutKind.Sequential)] -public struct Color -{ - public uint value; - - public Color(uint value) => this.value = value; - - public byte A => (byte)(value >> 24); - public byte R => (byte)(value >> 16); - public byte G => (byte)(value >> 8); - public byte B => (byte)value; - - public static Color FromArgb(byte a, byte r, byte g, byte b) => - new((uint)((a << 24) | (r << 16) | (g << 8) | b)); -} diff --git a/UIXrender/Interop/Drawing/ColorF.cs b/UIXrender/Interop/Drawing/ColorF.cs deleted file mode 100644 index f971e16..0000000 --- a/UIXrender/Interop/Drawing/ColorF.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.Render.ColorF (UIX.RenderApi/Microsoft/Iris/Render/ColorF.cs). -[StructLayout(LayoutKind.Sequential)] -public struct ColorF -{ - public float a; - public float r; - public float g; - public float b; -} diff --git a/UIXrender/Interop/Drawing/Point.cs b/UIXrender/Interop/Drawing/Point.cs deleted file mode 100644 index 858b657..0000000 --- a/UIXrender/Interop/Drawing/Point.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Point (UIX.RenderApi/Microsoft/Iris/Render/Point.cs). -[StructLayout(LayoutKind.Sequential)] -public struct Point -{ - public int x; - public int y; -} diff --git a/UIXrender/Interop/Drawing/Rectangle.cs b/UIXrender/Interop/Drawing/Rectangle.cs deleted file mode 100644 index 8d6d158..0000000 --- a/UIXrender/Interop/Drawing/Rectangle.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Rectangle (UIX.RenderApi/Microsoft/Iris/Render/Rectangle.cs). -[StructLayout(LayoutKind.Sequential)] -public struct Rectangle -{ - public int x; - public int y; - public int width; - public int height; -} - -// Bit-for-bit mirror of Microsoft.Iris.RenderAPI.Drawing.RectangleF -// (UIX/Microsoft/Iris/RenderAPI/Drawing/RectangleF.cs). -[StructLayout(LayoutKind.Sequential)] -public struct RectangleF -{ - public float x; - public float y; - public float width; - public float height; -} diff --git a/UIXrender/Interop/Drawing/Size.cs b/UIXrender/Interop/Drawing/Size.cs deleted file mode 100644 index bc46310..0000000 --- a/UIXrender/Interop/Drawing/Size.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Size (UIX.RenderApi/Microsoft/Iris/Render/Size.cs). -[StructLayout(LayoutKind.Sequential)] -public struct Size -{ - public int width; - public int height; - - public Size(int width, int height) - { - this.width = width; - this.height = height; - } -} diff --git a/UIXrender/Interop/Drawing/SizeF.cs b/UIXrender/Interop/Drawing/SizeF.cs deleted file mode 100644 index 6e9caba..0000000 --- a/UIXrender/Interop/Drawing/SizeF.cs +++ /dev/null @@ -1,11 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Drawing; - -// Bit-for-bit mirror of Microsoft.Iris.RenderAPI.Drawing.SizeF (UIX/Microsoft/Iris/RenderAPI/Drawing/SizeF.cs). -[StructLayout(LayoutKind.Sequential)] -public struct SizeF -{ - public float width; - public float height; -} diff --git a/UIXrender/Interop/EngineApi.cs b/UIXrender/Interop/EngineApi.cs deleted file mode 100644 index fdaa14e..0000000 --- a/UIXrender/Interop/EngineApi.cs +++ /dev/null @@ -1,221 +0,0 @@ -#if NETCOREAPP - -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop.Protocol; -using Microsoft.Iris.Render.Interop.Win32; - -namespace Microsoft.Iris.Render.Interop; - -// [UnmanagedCallersOnly] exports matching the core transport DllImport("UIXRender.dll") -// declarations in UIX.RenderApi/Microsoft/Iris/Render/Protocol/EngineApi.cs. This is -// the *only* place in UIXrender that deals with raw pointers/unmanaged function -// pointers -- everything it calls into (Microsoft.Iris.Render.Engine.EngineService) is -// ordinary, pointer-free managed C#. See logs/UIXrender/EngineCore.md for why -// SpInit/SpUninit are stubs and SpWrapBufferProc is a pass-through rather than a real -// wrapper, and logs/UIXrender/FullSurface.md for the rest of this file (added later). -public static unsafe class EngineApi -{ - [UnmanagedCallersOnly(EntryPoint = "SpInit")] - public static HRESULT SpInit(InitArgs* args) => HRESULT.S_OK; - - [UnmanagedCallersOnly(EntryPoint = "SpUninit")] - public static HRESULT SpUninit() => HRESULT.S_OK; - - [UnmanagedCallersOnly(EntryPoint = "SpWrapBufferProc")] - public static HRESULT SpWrapBufferProc(IntPtr pfnProcessBufferProc, IntPtr* ppNativeProc) - { - if (ppNativeProc == null) - return HRESULT.E_INVALIDARG; - - *ppNativeProc = pfnProcessBufferProc; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRenderThreadInit")] - public static HRESULT SpRenderThreadInit(InitArgs* argsRender, IntPtr* pThread) - { - if (argsRender == null || pThread == null) - return HRESULT.E_INVALIDARG; - - ContextID contextId = argsRender->idContext; - IntPtr nativeCallback = argsRender->pfnProcessBuffer; - IntPtr callbackData = argsRender->pvProcessData; - - // Adapts the raw native function pointer into the idiomatic BufferReceivedHandler - // shape EngineService deals in -- all pointer reconstruction for this direction - // lives here, not in Engine/. - BufferReceivedHandler handler = (source, bufferHandle, flags, data) => - { - if (nativeCallback == IntPtr.Zero) - return; - fixed (byte* pData = data) - { - var info = new BufferInfo - { - idContextSrc = source, - idContextDest = contextId, - idBuffer = bufferHandle, - nFlags = flags, - cbSizeBuffer = (uint)data.Length, - }; - var fn = (delegate* unmanaged)nativeCallback; - fn(callbackData, contextId.value, &info, pData); - } - }; - - IRenderThreadHandle thread = EngineService.StartRenderThread(contextId, handler); - *pThread = GCHandle.ToIntPtr(GCHandle.Alloc(thread, GCHandleType.Normal)); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRenderThreadUninit")] - public static HRESULT SpRenderThreadUninit(IntPtr pThread) - { - if (pThread == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - GCHandle handle = GCHandle.FromIntPtr(pThread); - (handle.Target as IDisposable)?.Dispose(); - handle.Free(); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpBufferOpen")] - public static HRESULT SpBufferOpen(BufferInfo* phdrData, void* pvData) - { - if (phdrData == null) - return HRESULT.E_INVALIDARG; - - var span = new ReadOnlySpan(pvData, (int)phdrData->cbSizeBuffer); - return EngineService.SendBuffer(phdrData->idContextSrc, phdrData->idContextDest, phdrData->idBuffer, phdrData->nFlags, span); - } - - // Real, but a documented no-op: this reimplementation has no Win32 message queue to - // pump (no window is created anywhere in this repo yet), and LocalChannel.Connect() - // -- the only path real Zune uses -- never calls this (see logs/UIXrender/EngineCore.md's - // SpInit/SpUninit note for the same "IGMM_STANDARD messaging model" open question). - // Always reports "no message available" rather than guessing at pump semantics. - [UnmanagedCallersOnly(EntryPoint = "SpPeekMessage")] - public static HRESULT SpPeekMessage(MSG* msg, HWND hwnd, uint nMsgFilterMin, uint nMsgFilterMax, uint wRemoveMsg, uint* nResult) - { - if (msg == null || nResult == null) - return HRESULT.E_INVALIDARG; - *msg = default; - *nResult = EngineService.PeekMessage(nMsgFilterMin, nMsgFilterMax, wRemoveMsg); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpWaitMessage")] - public static HRESULT SpWaitMessage(uint nTimeOutMs, IntPtr unused) - { - EngineService.WaitMessage(nTimeOutMs); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpInvoke")] - public static HRESULT SpInvoke(ContextID idContext, IntPtr pfnInvoke, IntPtr pvArgs, int synchronous) => - EngineService.Invoke(idContext, pfnInvoke, pvArgs, synchronous != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpRemoteCreateServerStreams")] - public static HRESULT SpRemoteCreateServerStreams(char* stSession, TransportProtocol nProtocol, IntPtr* pSendStream, IntPtr* pReceiveStream) - { - if (pSendStream == null || pReceiveStream == null) - return HRESULT.E_INVALIDARG; - - HRESULT hr = EngineService.RemoteCreateServerStreams(NativeString.UniToString(stSession), nProtocol, out IntPtr send, out IntPtr receive); - *pSendStream = send; - *pReceiveStream = receive; - return hr; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRemoteWaitServerStreamsConnected")] - public static HRESULT SpRemoteWaitServerStreamsConnected(TransportProtocol nProtocol, IntPtr pSendStream, IntPtr pReceiveStream) => - EngineService.RemoteWaitServerStreamsConnected(nProtocol, pSendStream); - - [UnmanagedCallersOnly(EntryPoint = "SpRemoteServerInit")] - public static HRESULT SpRemoteServerInit(IntPtr pSendStream, IntPtr pReceiveStream, InitArgs argsSend, IntPtr* pSession) - { - if (pSession == null) - return HRESULT.E_INVALIDARG; - - // Adapt the raw native process-buffer function pointer into a BufferReceivedHandler, - // exactly as SpRenderThreadInit does -- RemoteServerConnection deals only in the - // pointer-free handler shape. - ContextID contextId = argsSend.idContext; - IntPtr nativeCallback = argsSend.pfnProcessBuffer; - IntPtr callbackData = argsSend.pvProcessData; - BufferReceivedHandler handler = nativeCallback == IntPtr.Zero - ? null - : (source, bufferHandle, flags, data) => - { - fixed (byte* pData = data) - { - var info = new BufferInfo - { - idContextSrc = source, - idContextDest = contextId, - idBuffer = bufferHandle, - nFlags = flags, - cbSizeBuffer = (uint)data.Length, - }; - var fn = (delegate* unmanaged)nativeCallback; - fn(callbackData, source.value, &info, pData); - } - }; - - HRESULT hr = EngineService.RemoteServerInit(pSendStream, contextId, handler, out IntPtr session); - *pSession = session; - return hr; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRemoteServerUninit")] - public static HRESULT SpRemoteServerUninit(IntPtr pSession, int fForceShutdown, ShutdownReason* nShutdownReason) - { - if (nShutdownReason == null) - return HRESULT.E_INVALIDARG; - - HRESULT hr = EngineService.RemoteServerUninit(pSession, fForceShutdown != 0, out ShutdownReason reason); - *nShutdownReason = reason; - return hr; - } - - // Deliberately not implemented: HLSL effect compilation exists only as a - // graphics-API-specific service (d3dcompiler_47.dll / Silk.NET.Direct3D.Compilers), - // and Silk.NET has no cross-platform abstraction over it -- so per this project's - // "abstractions, not specific graphics APIs" dependency policy there is nothing - // correct to call here yet. Reports failure honestly (with empty out-params, so a - // caller that ignores the HRESULT still sees a well-defined "no blob") rather than - // pretending success. See logs/UIXrender/FullSurface.md, decision 1. - // TODO: revisit once this project has a rendering backend of its own to compile for. - [UnmanagedCallersOnly(EntryPoint = "SpDx9CompileEffect")] - public static HRESULT SpDx9CompileEffect(byte* stEffect, byte* stDefines, IntPtr* pErrorString, IntPtr* pErrorBuffer, IntPtr* pEffectBlob, uint* effectBlobSize, IntPtr* pEffectBlobBuffer) - { - // Empty out-params so a caller ignoring the HRESULT still sees a well-defined - // "no blob"; the not-implemented decision itself lives in EngineService. - if (pErrorString != null) *pErrorString = IntPtr.Zero; - if (pErrorBuffer != null) *pErrorBuffer = IntPtr.Zero; - if (pEffectBlob != null) *pEffectBlob = IntPtr.Zero; - if (effectBlobSize != null) *effectBlobSize = 0; - if (pEffectBlobBuffer != null) *pEffectBlobBuffer = IntPtr.Zero; - return EngineService.Dx9CompileEffect(); - } - - // Generic COM release: calls IUnknown::Release through the object's own vtable - // (slot 2, after QueryInterface/AddRef) rather than assuming anything about what - // concrete type pUnknown is -- this export exists precisely to release arbitrary - // IUnknown-shaped pointers handed across the boundary. - [UnmanagedCallersOnly(EntryPoint = "SpObjectRelease")] - public static void SpObjectRelease(IntPtr pUnknown) - { - if (pUnknown == IntPtr.Zero) - return; - void* vtbl = *(void**)pUnknown; - var release = (delegate* unmanaged)(*((void**)vtbl + 2)); - release(pUnknown); - } -} - -#endif diff --git a/UIXrender/Interop/Extensions/ImageTypes.cs b/UIXrender/Interop/Extensions/ImageTypes.cs deleted file mode 100644 index 9c4ede9..0000000 --- a/UIXrender/Interop/Extensions/ImageTypes.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Interop.Extensions; - -// Bit-for-bit mirrors of the ExtensionsApi.cs (UIX.RenderApi/Microsoft/Iris/Render/Extensions/*.cs) -// asset-loading types: ImageRequirements, ImageHeader, ImageData, ImageInformation, -// HSpBitmap, SurfaceFormat. - -public enum SurfaceFormat : uint -{ - None = 0, - Bpp8 = 0x00080000, - A8 = 0x00088000, - Bpp16 = 0x00100000, - RGB16_555 = 0x00100555, - RGB16_565 = 0x00100565, - ARGB16_1555 = 0x00101555, - Bpp24 = 0x00180000, - RGB24 = 0x00180888, - Bpp32 = 0x00200000, - RGB32 = 0x00200888, - ARGB32 = 0x00208888, - YUY2 = 0x21100000, - External = 0x80000000, -} - -[Flags] -public enum ImageRequirementsFields -{ - None = 0, - MaximumSize = 1, - Border = 2, - Flippable = 4, - AntialiasEdges = 16, -} - -[StructLayout(LayoutKind.Sequential)] -public struct ImageRequirements -{ - public ImageRequirementsFields mask; - public Size maximumSizePxl; - public int borderPxl; - public ColorF borderColor; -} - -[StructLayout(LayoutKind.Sequential)] -public struct ImageHeader -{ - public Size sizeActualPxl; - public Size sizeOriginalPxl; - public int stride; - public SurfaceFormat format; -} - -[StructLayout(LayoutKind.Sequential)] -public struct ImageData -{ - public IntPtr rgData; -} - -[StructLayout(LayoutKind.Sequential)] -public struct ImageInformation -{ - public ImageHeader header; - public ImageData data; -} - -[Flags] -public enum BitmapOptions -{ - None = 0, - Decode = 1, - Flip = 2, - Valid = Flip | Decode, -} - -[StructLayout(LayoutKind.Sequential)] -public struct HSpBitmap -{ - public IntPtr h; - public static readonly HSpBitmap NULL = new(); -} diff --git a/UIXrender/Interop/Extensions/SoundTypes.cs b/UIXrender/Interop/Extensions/SoundTypes.cs deleted file mode 100644 index c5740aa..0000000 --- a/UIXrender/Interop/Extensions/SoundTypes.cs +++ /dev/null @@ -1,47 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Extensions; - -// Bit-for-bit mirrors of ExtensionsApi.cs's sound types. -[Flags] -public enum SoundOptions -{ - None = 0, - Decode = 1, - BigEndian = 2, - Valid = BigEndian | Decode, -} - -[StructLayout(LayoutKind.Sequential)] -public struct SoundHeader -{ - public ushort formatTag; - public ushort channels; - public uint samplesPerSec; - public uint avgBytesPerSec; - public ushort blockAlign; - public ushort bitsPerSample; - public ushort cbExtraData; - public uint cbDataSize; -} - -[StructLayout(LayoutKind.Sequential)] -public struct SoundData -{ - public IntPtr rgData; -} - -[StructLayout(LayoutKind.Sequential)] -public struct SoundInformation -{ - public SoundHeader header; - public SoundData data; -} - -[StructLayout(LayoutKind.Sequential)] -public struct HSpSound -{ - public IntPtr h; - public static readonly HSpSound NULL = new(); -} diff --git a/UIXrender/Interop/HRESULT.cs b/UIXrender/Interop/HRESULT.cs deleted file mode 100644 index eca1979..0000000 --- a/UIXrender/Interop/HRESULT.cs +++ /dev/null @@ -1,31 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of the decompiled Microsoft.Iris.Render.Internal.HRESULT -// (UIX.RenderApi/Microsoft/Iris/Render/Internal/HRESULT.cs) -- a single int, so every -// [UnmanagedCallersOnly] method returning HRESULT here matches what EngineApi.cs's -// [DllImport] declarations expect byte-for-byte. The S_OK/E_* constants aren't part of -// the original decompiled surface (nothing to break by adding them -- this is new code -// with no existing dependents), added here purely as return-value conveniences. -[StructLayout(LayoutKind.Sequential)] -public struct HRESULT -{ - public static readonly HRESULT S_OK = new(0); - public static readonly HRESULT E_FAIL = new(unchecked((int)0x80004005)); - public static readonly HRESULT E_INVALIDARG = new(unchecked((int)0x80070057)); - public static readonly HRESULT E_NOTIMPL = new(unchecked((int)0x80004001)); - - public int hr; - - public HRESULT(int hr) => this.hr = hr; - - public static bool operator ==(HRESULT a, HRESULT b) => a.hr == b.hr; - public static bool operator !=(HRESULT a, HRESULT b) => a.hr != b.hr; - - public override bool Equals(object obj) => obj is HRESULT other && hr == other.hr; - public override int GetHashCode() => hr; - - public bool IsError() => hr < 0; - public bool IsSuccess() => hr >= 0; -} diff --git a/UIXrender/Interop/InitArgs.cs b/UIXrender/Interop/InitArgs.cs deleted file mode 100644 index a7c6d57..0000000 --- a/UIXrender/Interop/InitArgs.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of EngineApi.InitArgs -// (UIX.RenderApi/Microsoft/Iris/Render/Protocol/EngineApi.cs). The managed side declares -// pfnTimeout as a delegate (TimeoutEventHandler); the CLR marshals a delegate field to a -// plain function pointer before the struct crosses into native code, so it's IntPtr here -// -- same wire bytes, no delegate type on our side. -[StructLayout(LayoutKind.Sequential)] -public struct InitArgs -{ - public uint cbSize; - public ContextID idContext; - public int cItemsPerGroupBits; - public int cGroupBits; - public IntPtr pfnProcessBuffer; - public IntPtr pvProcessData; - public RENDERHANDLE idObjectBrokerClass; - public IntPtr pfnTimeout; - public IntPtr pvTimeoutData; - public uint nTimeOutSec; -} diff --git a/UIXrender/Interop/Protocol/RemoteChannelTypes.cs b/UIXrender/Interop/Protocol/RemoteChannelTypes.cs deleted file mode 100644 index 626a1f1..0000000 --- a/UIXrender/Interop/Protocol/RemoteChannelTypes.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace Microsoft.Iris.Render.Interop.Protocol; - -// Bit-for-bit mirrors of UIX.RenderApi/Microsoft/Iris/Render/Protocol/{ShutdownReason,TransportProtocol}.cs. -public enum ShutdownReason -{ - NoReason, - SelfShutdown, - PeerShutdown, - TransportClosed, - TransportFailure, - GenericFailure, -} - -public enum TransportProtocol -{ - None = 0, - Min = 1, - VC = 1, - TCP = 2, - UDP = 3, - Max = 4, - PIPE = 4, -} diff --git a/UIXrender/Interop/RENDERHANDLE.cs b/UIXrender/Interop/RENDERHANDLE.cs deleted file mode 100644 index 470da27..0000000 --- a/UIXrender/Interop/RENDERHANDLE.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Protocol.RENDERHANDLE -// (UIX.RenderApi/Microsoft/Iris/Render/Protocol/RENDERHANDLE.cs). -[StructLayout(LayoutKind.Sequential)] -public struct RENDERHANDLE -{ - public static readonly RENDERHANDLE NULL = new(0); - - public uint value; - - public RENDERHANDLE(uint value) => this.value = value; - - public static bool operator ==(RENDERHANDLE a, RENDERHANDLE b) => a.value == b.value; - public static bool operator !=(RENDERHANDLE a, RENDERHANDLE b) => a.value != b.value; - - public override bool Equals(object obj) => obj is RENDERHANDLE other && value == other.value; - public override int GetHashCode() => (int)value; -} diff --git a/UIXrender/Interop/Text/RasterizeRunPacket.cs b/UIXrender/Interop/Text/RasterizeRunPacket.cs deleted file mode 100644 index e1ffb8f..0000000 --- a/UIXrender/Interop/Text/RasterizeRunPacket.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop.Drawing; -using Microsoft.Iris.Render.Interop.Win32; - -namespace Microsoft.Iris.Render.Interop.Text; - -// Bit-for-bit mirror of NativeApi.RasterizeRunPacket/UnderlineStyle -// (UIX/Microsoft/Iris/OS/NativeApi.cs). -public enum UnderlineStyle -{ - None, - Solid, - Thick, - Dotted, - Dash, - DashDot, - DashDotDot, -} - -[StructLayout(LayoutKind.Sequential)] -public struct RasterizeRunPacket -{ - public Rectangle rcLayoutBounds; - public RectangleF rcfRenderBounds; - public int naturalX; - public int naturalY; - public int rasterizeX; - public int rasterizeY; - public byte aaConfig; - public Color clrText; - public Color clrBackground; - public int fontFaceUniqueId; - public LOGFONTW lf; - public Size sizeRasterizeRun; - public Size sizeNatural; - public int ascenderInset; - public int baselineInset; - public int lineNumber; - public int effects; - public UnderlineStyle underlineStyle; - public Rectangle rcUnderlineBounds; -} diff --git a/UIXrender/Interop/Text/TextMeasureParamsData.cs b/UIXrender/Interop/Text/TextMeasureParamsData.cs deleted file mode 100644 index 0536f57..0000000 --- a/UIXrender/Interop/Text/TextMeasureParamsData.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Interop.Text; - -// Bit-for-bit mirror of Microsoft.Iris.Drawing.TextMeasureParams.MarshalledData/FormattedRange -// (UIX/Microsoft/Iris/Drawing/TextMeasureParams.cs). -[System.Flags] -public enum TextMeasureFlags : byte -{ - None = 0, - Content = 1, - IsRtl = 2, - WordWrap = 4, - WordWrapValue = 8, - PasswordMasked = 16, - TrimLeftSideBearing = 32, - FormatOnly = 64, -} - -[StructLayout(LayoutKind.Sequential)] -public struct FormattedRange -{ - public int firstCharacter; - public int lastCharacter; - public Color color; - public int styleIndex; -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct TextMeasureParamsData -{ - public TextMeasureFlags flags; - public byte alignment; - public char passwordChar; - public char* content; - public float scale; - public SizeF constraint; - public TextStyleData* pTextStyle; - public int formattedRangeCount; - public FormattedRange* pFormattedRanges; - public int formattedRangeStylesCount; - public TextStyleData* pFormattedRangeStyles; -} diff --git a/UIXrender/Interop/Text/TextStyleData.cs b/UIXrender/Interop/Text/TextStyleData.cs deleted file mode 100644 index 1593e7b..0000000 --- a/UIXrender/Interop/Text/TextStyleData.cs +++ /dev/null @@ -1,38 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Interop.Text; - -// Bit-for-bit mirror of Microsoft.Iris.Drawing.TextStyle.MarshalledData -// (UIX/Microsoft/Iris/Drawing/TextStyle.cs). -[System.Flags] -public enum TextStyleSetFlags -{ - None = 0, - FontFace = 1, - FontHeight = 2, - Bold = 4, - Italic = 8, - Underline = 16, - LineSpacing = 32, - TextColor = 64, - EnableKerning = 128, - CharacterSpacing = 256, - AltFontHeight = 512, - BoldValue = 65536, - ItalicValue = 131072, - UnderlineValue = 262144, - EnableKerningValue = 524288, -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct TextStyleData -{ - public TextStyleSetFlags flags; - public char* fontFace; - public float fontHeightPts; - public float altFontHeightPts; - public float lineSpacing; - public float characterSpacing; - public Color textColor; -} diff --git a/UIXrender/Interop/UIXVariant.cs b/UIXrender/Interop/UIXVariant.cs deleted file mode 100644 index eff5b1e..0000000 --- a/UIXrender/Interop/UIXVariant.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop; - -// Bit-for-bit mirror of Microsoft.Iris.CodeModel.Cpp.UIXVariant -// (UIX/Microsoft/Iris/CodeModel/Cpp/UIXVariant.cs) -- a tagged union: a 4-byte type tag -// followed by an 8-byte payload reinterpreted depending on the tag. Managed-side helpers -// (GetValue/SetXxxValue) are reimplemented here as real logic rather than mirrored -// method-for-method, since this side never needs to talk to the CLR's own marshaller -- -// it just needs the same wire layout and equivalent behavior. -public enum VariantType -{ - Empty = 0, - Bool = 1, - Byte = 2, - Int32 = 3, - Int64 = 4, - Single = 5, - Double = 6, - Enum = 7, - UIXObject = 128, - UIXString = 129, - UIXImage = 130, - UIXDataQuery = 131, - UIXDataType = 132, -} - -[StructLayout(LayoutKind.Sequential)] -public struct EnumValue -{ - public int value; - public uint type; -} - -[StructLayout(LayoutKind.Sequential)] -public unsafe struct UIXVariant -{ - public VariantType type; - public long union; - - public static UIXVariant Empty => new() { type = VariantType.Empty }; - - public IntPtr AsPointer - { - get => (IntPtr)union; - set => union = (long)value; - } - - public bool AsBool - { - get => union != 0; - set => union = value ? 1 : 0; - } - - public byte AsByte - { - get => (byte)union; - set => union = value; - } - - public int AsInt32 - { - get => (int)union; - set => union = value; - } - - public long AsInt64 - { - get => union; - set => union = value; - } - - public float AsSingle - { - get { long u = union; return *(float*)&u; } - set { float f = value; union = *(int*)&f; } - } - - public double AsDouble - { - get { long u = union; return *(double*)&u; } - set => union = BitConverter.DoubleToInt64Bits(value); - } - - public EnumValue AsEnum - { - get { long u = union; return *(EnumValue*)&u; } - set { long u = 0; *(EnumValue*)&u = value; union = u; } - } - - public static UIXVariant FromObject(object value) => value switch - { - null => Empty, - bool b => new UIXVariant { type = VariantType.Bool, AsBool = b }, - byte b => new UIXVariant { type = VariantType.Byte, AsByte = b }, - int i => new UIXVariant { type = VariantType.Int32, AsInt32 = i }, - long l => new UIXVariant { type = VariantType.Int64, AsInt64 = l }, - float f => new UIXVariant { type = VariantType.Single, AsSingle = f }, - double d => new UIXVariant { type = VariantType.Double, AsDouble = d }, - _ => throw new NotSupportedException($"Cannot convert {value.GetType()} to UIXVariant"), - }; - - public object ToObject() => type switch - { - VariantType.Empty => null, - VariantType.Bool => AsBool, - VariantType.Byte => AsByte, - VariantType.Int32 => AsInt32, - VariantType.Int64 => AsInt64, - VariantType.Single => AsSingle, - VariantType.Double => AsDouble, - _ => throw new NotSupportedException($"Cannot convert UIXVariant of type {type} to object"), - }; -} diff --git a/UIXrender/Interop/Win32/HANDLE.cs b/UIXrender/Interop/Win32/HANDLE.cs deleted file mode 100644 index 9a2b39a..0000000 --- a/UIXrender/Interop/Win32/HANDLE.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Win32; - -// Bit-for-bit mirror of Microsoft.Iris.OS.Win32Api.HANDLE (UIX/Microsoft/Iris/OS/Win32Api.cs) -// -- the opaque handle type NativeApi.cs's rich-text/simple-text exports use (hRto/hSto). -[StructLayout(LayoutKind.Sequential)] -public struct HANDLE -{ - public IntPtr h; - public static readonly HANDLE NULL = new() { h = IntPtr.Zero }; -} diff --git a/UIXrender/Interop/Win32/HWND.cs b/UIXrender/Interop/Win32/HWND.cs deleted file mode 100644 index a5d73d2..0000000 --- a/UIXrender/Interop/Win32/HWND.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Win32; - -// Bit-for-bit mirror of Microsoft.Iris.Render.HWND (UIX.RenderApi/Microsoft/Iris/Render/HWND.cs). -[StructLayout(LayoutKind.Sequential)] -public struct HWND -{ - public IntPtr h; - public static readonly HWND NULL = new() { h = IntPtr.Zero }; -} diff --git a/UIXrender/Interop/Win32/LOGFONTW.cs b/UIXrender/Interop/Win32/LOGFONTW.cs deleted file mode 100644 index 4eb5f2a..0000000 --- a/UIXrender/Interop/Win32/LOGFONTW.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Win32; - -// Bit-for-bit mirror of Microsoft.Iris.OS.Win32Api.LOGFONTW_STRUCT -// (UIX/Microsoft/Iris/OS/Win32Api.cs), including its 32-char inline face-name buffer. -[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] -public unsafe struct LOGFONTW -{ - public const int LF_FACESIZE = 32; - - public int lfHeight; - public int lfWidth; - public int lfEscapement; - public int lfOrientation; - public int lfWeight; - public byte lfItalic; - public byte lfUnderline; - public byte lfStrikeOut; - public byte lfCharSet; - public byte lfOutPrecision; - public byte lfClipPrecision; - public byte lfQuality; - public byte lfPitchAndFamily; - public fixed char lfFaceName[LF_FACESIZE]; -} diff --git a/UIXrender/Interop/Win32/MSG.cs b/UIXrender/Interop/Win32/MSG.cs deleted file mode 100644 index b2c4662..0000000 --- a/UIXrender/Interop/Win32/MSG.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System; -using System.Runtime.InteropServices; - -namespace Microsoft.Iris.Render.Interop.Win32; - -// Bit-for-bit mirror of Microsoft.Iris.Render.Internal.Win32Api.MSG -// (UIX.RenderApi/Microsoft/Iris/Render/Internal/Win32Api.cs) -- the variant EngineApi.cs's -// SpPeekMessage actually resolves to (hwnd is the Render-namespace HWND wrapper, not a bare IntPtr). -[StructLayout(LayoutKind.Sequential)] -public struct MSG -{ - public HWND hwnd; - public uint message; - public IntPtr wParam; - public IntPtr lParam; - public uint time; - public int pt_x; - public int pt_y; -} diff --git a/UIXrender/Interop/XmlLite/NativeXmlNodeType.cs b/UIXrender/Interop/XmlLite/NativeXmlNodeType.cs deleted file mode 100644 index 5f3b856..0000000 --- a/UIXrender/Interop/XmlLite/NativeXmlNodeType.cs +++ /dev/null @@ -1,17 +0,0 @@ -namespace Microsoft.Iris.Render.Interop.XmlLite; - -// Bit-for-bit mirror of Microsoft.Iris.OS.NativeXmlNodeType (UIX/Microsoft/Iris/OS/NativeXmlNodeType.cs). -public enum NativeXmlNodeType -{ - None = 0, - Element = 1, - Attribute = 2, - Text = 3, - CDATA = 4, - ProcessingInstruction = 7, - Comment = 8, - DocumentType = 10, - Whitespace = 13, - EndElement = 15, - XmlDeclaration = 17, -} diff --git a/UIXrender/Subsystems/Assets/BitmapStore.cs b/UIXrender/Subsystems/Assets/BitmapStore.cs deleted file mode 100644 index 160ea01..0000000 --- a/UIXrender/Subsystems/Assets/BitmapStore.cs +++ /dev/null @@ -1,95 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop.Drawing; -using Microsoft.Iris.Render.Interop.Extensions; -using StbImageSharp; - -namespace Microsoft.Iris.Render.Subsystems.Assets; - -// Backing store for the SpBitmap* family (UIX.RenderApi/.../Extensions/ExtensionsApi.cs). -// -// Decode is real and cross-platform: StbImageSharp is pure managed (PNG/JPEG/BMP/TGA/GIF/ -// PSD), so this works identically on Windows and Linux and stays NativeAOT-safe, unlike -// System.Drawing/GDI+. See logs/UIXrender/FullSurface.md, decision 3. -// -// Pixels always land as ARGB32 in a single unmanaged allocation the caller can read -// through ImageInformation.Data.rgData until SpBitmapDelete. Stb decodes to RGBA byte -// order; the Iris SurfaceFormat.ARGB32 the managed side expects is BGRA in memory on a -// little-endian machine (0xAARRGGBB as a uint), so the channel swap below is required, -// not incidental. -internal sealed class LoadedBitmap : IDisposable -{ - private IntPtr _pixels; - - private LoadedBitmap(IntPtr pixels, ImageHeader header) - { - _pixels = pixels; - Header = header; - } - - public ImageHeader Header { get; } - - public ImageInformation ToInformation() => new() - { - header = Header, - data = new ImageData { rgData = _pixels }, - }; - - public static LoadedBitmap FromDecoded(ImageResult image, in ImageRequirements requirements) - { - int width = image.Width; - int height = image.Height; - int stride = width * 4; - - IntPtr buffer = Marshal.AllocHGlobal(stride * height); - unsafe - { - var dest = (byte*)buffer; - byte[] src = image.Data; - for (int i = 0; i < width * height; i++) - { - // RGBA (stb) -> BGRA in memory == 0xAARRGGBB as a little-endian uint. - dest[i * 4 + 0] = src[i * 4 + 2]; - dest[i * 4 + 1] = src[i * 4 + 1]; - dest[i * 4 + 2] = src[i * 4 + 0]; - dest[i * 4 + 3] = src[i * 4 + 3]; - } - } - - var header = new ImageHeader - { - sizeActualPxl = new Size(width, height), - sizeOriginalPxl = new Size(width, height), - stride = stride, - format = SurfaceFormat.ARGB32, - }; - - return new LoadedBitmap(buffer, header); - } - - public static unsafe LoadedBitmap FromRaw(Size sizeActualPxl, int stride, SurfaceFormat format, IntPtr source) - { - int byteCount = stride * sizeActualPxl.height; - IntPtr buffer = Marshal.AllocHGlobal(byteCount); - Buffer.MemoryCopy((void*)source, (void*)buffer, byteCount, byteCount); - - var header = new ImageHeader - { - sizeActualPxl = sizeActualPxl, - sizeOriginalPxl = sizeActualPxl, - stride = stride, - format = format, - }; - - return new LoadedBitmap(buffer, header); - } - - public void Dispose() - { - if (_pixels != IntPtr.Zero) - { - Marshal.FreeHGlobal(_pixels); - _pixels = IntPtr.Zero; - } - } -} diff --git a/UIXrender/Subsystems/Assets/ExtensionsApi.cs b/UIXrender/Subsystems/Assets/ExtensionsApi.cs deleted file mode 100644 index 41f9f60..0000000 --- a/UIXrender/Subsystems/Assets/ExtensionsApi.cs +++ /dev/null @@ -1,154 +0,0 @@ -#if NETCOREAPP - -using System; -using System.IO; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Drawing; -using Microsoft.Iris.Render.Interop.Extensions; -using StbImageSharp; - -namespace Microsoft.Iris.Render.Subsystems.Assets; - -// [UnmanagedCallersOnly] exports matching the asset-loading DllImports in -// UIX.RenderApi/Microsoft/Iris/Render/Extensions/ExtensionsApi.cs. -// -// Note ImageRequirements is a *class* with [StructLayout(Sequential)] on the managed side -// and is passed [MarshalAs(UnmanagedType.LPStruct)], i.e. the callee receives a pointer -// to its fields -- hence ImageRequirements* here, not a by-value struct. -public static unsafe class ExtensionsApi -{ - [UnmanagedCallersOnly(EntryPoint = "SpBitmapLoadFile")] - public static HRESULT SpBitmapLoadFile(char* stFileName, ImageRequirements* req, BitmapOptions nOptions, HSpBitmap* hBmp, ImageInformation* info) - { - if (hBmp == null || info == null) - return HRESULT.E_INVALIDARG; - - string path = NativeString.UniToString(stFileName); - if (string.IsNullOrEmpty(path) || !File.Exists(path)) - return HRESULT.E_FAIL; - - try - { - using FileStream stream = File.OpenRead(path); - return CompleteDecode(ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha), req, hBmp, info); - } - catch (Exception e) when (e is IOException or ArgumentException) - { - return HRESULT.E_FAIL; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpBitmapLoadBuffer")] - public static HRESULT SpBitmapLoadBuffer(IntPtr pvSrc, uint cbSize, ImageRequirements* req, BitmapOptions nOptions, HSpBitmap* hBmp, ImageInformation* info) - { - if (hBmp == null || info == null || pvSrc == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - try - { - using var stream = new UnmanagedMemoryStream((byte*)pvSrc, cbSize); - return CompleteDecode(ImageResult.FromStream(stream, ColorComponents.RedGreenBlueAlpha), req, hBmp, info); - } - catch (Exception e) when (e is IOException or ArgumentException) - { - return HRESULT.E_FAIL; - } - } - - // Raw pixels need no decoder -- this path just takes ownership of a copy, so it is - // fully real on every platform. - [UnmanagedCallersOnly(EntryPoint = "SpBitmapLoadRaw")] - public static HRESULT SpBitmapLoadRaw(Size sizeActualPxl, int nStride, SurfaceFormat nFormat, IntPtr pvData, ImageRequirements* req, BitmapOptions nOptions, HSpBitmap* hBmp, ImageInformation* info) - { - if (hBmp == null || info == null || pvData == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - var bitmap = LoadedBitmap.FromRaw(sizeActualPxl, nStride, nFormat, pvData); - hBmp->h = HandleTable.Alloc(bitmap); - *info = bitmap.ToInformation(); - return HRESULT.S_OK; - } - - // Win32 resource sections (HINSTANCE + resource name) are a PE/Windows loader concept - // with no cross-platform equivalent, and this project has no resource-section reader - // of its own yet. Reports failure rather than inventing a lookup. - // TODO: implement once SpLoadBinaryResource's resource store (Subsystems/Modules) - // gains real PE resource parsing -- this should then read through that, not duplicate it. - [UnmanagedCallersOnly(EntryPoint = "SpBitmapLoadResource")] - public static HRESULT SpBitmapLoadResource(IntPtr hinst, char* stName, int nType, ImageRequirements* req, BitmapOptions nOptions, HSpBitmap* hBmp, ImageInformation* info) - { - if (hBmp == null || info == null) - return HRESULT.E_INVALIDARG; - - *hBmp = HSpBitmap.NULL; - *info = default; - return HRESULT.E_NOTIMPL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpBitmapDelete")] - public static HRESULT SpBitmapDelete(HSpBitmap hBmp) - { - HandleTable.Free(hBmp.h); - return HRESULT.S_OK; - } - - // Real: parses a RIFF/WAVE container and hands back the PCM payload. No codec is - // needed because the managed side's only declared format constant is - // WAVE_FORMAT_PCM (ExtensionsApi.cs), i.e. uncompressed samples. - [UnmanagedCallersOnly(EntryPoint = "SpSoundLoadBuffer")] - public static HRESULT SpSoundLoadBuffer(IntPtr pBuffer, int dwSize, SoundOptions options, HSpSound* hSound, SoundInformation* info) - { - if (hSound == null || info == null || pBuffer == IntPtr.Zero || dwSize <= 0) - return HRESULT.E_INVALIDARG; - - var source = new ReadOnlySpan((void*)pBuffer, dwSize); - if (!WaveParser.TryParse(source, out SoundHeader header, out byte[] samples)) - return HRESULT.E_FAIL; - - IntPtr payload = Marshal.AllocHGlobal(samples.Length); - Marshal.Copy(samples, 0, payload, samples.Length); - - var sound = new LoadedSound(payload); - hSound->h = HandleTable.Alloc(sound); - *info = new SoundInformation { header = header, data = new SoundData { rgData = payload } }; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpSoundDispose")] - public static HRESULT SpSoundDispose(HSpSound hSound, SoundInformation info) - { - HandleTable.Free(hSound.h); - return HRESULT.S_OK; - } - - private static HRESULT CompleteDecode(ImageResult image, ImageRequirements* req, HSpBitmap* hBmp, ImageInformation* info) - { - if (image == null) - return HRESULT.E_FAIL; - - ImageRequirements requirements = req != null ? *req : default; - var bitmap = LoadedBitmap.FromDecoded(image, requirements); - hBmp->h = HandleTable.Alloc(bitmap); - *info = bitmap.ToInformation(); - return HRESULT.S_OK; - } -} - -internal sealed class LoadedSound(IntPtr samples) : IDisposable -{ - private IntPtr _samples = samples; - - public void Dispose() - { - if (_samples != IntPtr.Zero) - { - Marshal.FreeHGlobal(_samples); - _samples = IntPtr.Zero; - } - } -} - -#endif diff --git a/UIXrender/Subsystems/Assets/WaveParser.cs b/UIXrender/Subsystems/Assets/WaveParser.cs deleted file mode 100644 index 64e2c5a..0000000 --- a/UIXrender/Subsystems/Assets/WaveParser.cs +++ /dev/null @@ -1,121 +0,0 @@ -using System; -using System.Buffers.Binary; -using Microsoft.Iris.Render.Interop.Extensions; - -namespace Microsoft.Iris.Render.Subsystems.Assets; - -// Real RIFF/WAVE container parser for SpSoundLoadBuffer. Deliberately minimal and -// allocation-light: it locates the "fmt " and "data" chunks, fills the SoundHeader the -// managed side already declares (ExtensionsApi.SoundHeader -- field-for-field a -// WAVEFORMATEX plus the data size), and returns the raw sample bytes. -internal static class WaveParser -{ - private const ushort WAVE_FORMAT_PCM = 1; - -#if NETCOREAPP - public static bool TryParse(ReadOnlySpan source, out SoundHeader header, out byte[] samples) - { - header = default; - samples = Array.Empty(); - - // "RIFF" "WAVE" then a chunk list. - if (source.Length < 12 || - !source[..4].SequenceEqual("RIFF"u8) || - !source.Slice(8, 4).SequenceEqual("WAVE"u8)) - return false; - - bool haveFormat = false; - int offset = 12; - - while (offset + 8 <= source.Length) - { - ReadOnlySpan chunkId = source.Slice(offset, 4); - uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(offset + 4, 4)); - int body = offset + 8; - - if (body + chunkSize > source.Length) - chunkSize = (uint)(source.Length - body); - - if (chunkId.SequenceEqual("fmt "u8) && chunkSize >= 16) - { - ReadOnlySpan fmt = source.Slice(body, (int)chunkSize); - header.formatTag = BinaryPrimitives.ReadUInt16LittleEndian(fmt[..2]); - header.channels = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(2, 2)); - header.samplesPerSec = BinaryPrimitives.ReadUInt32LittleEndian(fmt.Slice(4, 4)); - header.avgBytesPerSec = BinaryPrimitives.ReadUInt32LittleEndian(fmt.Slice(8, 4)); - header.blockAlign = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(12, 2)); - header.bitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(14, 2)); - header.cbExtraData = chunkSize >= 18 ? BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(16, 2)) : (ushort)0; - haveFormat = true; - } - else if (chunkId.SequenceEqual("data"u8)) - { - samples = source.Slice(body, (int)chunkSize).ToArray(); - header.cbDataSize = chunkSize; - } - - // Chunks are word-aligned: an odd-sized chunk is followed by a pad byte. - offset = body + (int)chunkSize + ((chunkSize & 1) != 0 ? 1 : 0); - } - - // Only uncompressed PCM is representable through this surface -- the managed side - // declares WAVE_FORMAT_PCM as its sole format constant, so anything else would be - // silently misinterpreted downstream rather than merely unsupported. - return haveFormat && samples.Length > 0 && header.formatTag == WAVE_FORMAT_PCM; - } -#endif - - public static bool TryParse(byte[] source, out SoundHeader header, out byte[] samples) - { - header = default; - samples = Array.Empty(); - - // "RIFF" "WAVE" then a chunk list. - if (source.Length < 12 || - !(source[0] == 'R' && source[1] == 'I' && source[2] == 'F' && source[3] == 'F') || - !(source[8] == 'W' && source[9] == 'A' && source[10] == 'V' && source[11] == 'E')) - return false; - - bool haveFormat = false; - int offset = 12; - - while (offset + 8 <= source.Length) - { - string chunkId = System.Text.Encoding.UTF8.GetString(source, offset, 4); - int body = offset + 8; - - var chunkSizeOffset = offset + 4; - //uint chunkSize = source[chunkSizeOffset] | source[chunkSizeOffset + 1]; - uint chunkSize = BinaryPrimitives.ReadUInt32LittleEndian(source.Slice(offset + 4, 4)); - - if (body + chunkSize > source.Length) - chunkSize = (uint)(source.Length - body); - - if (chunkId == "fmt " && chunkSize >= 16) - { - ReadOnlySpan fmt = source.Slice(body, (int)chunkSize); - header.formatTag = BinaryPrimitives.ReadUInt16LittleEndian(fmt[..2]); - header.channels = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(2, 2)); - header.samplesPerSec = BinaryPrimitives.ReadUInt32LittleEndian(fmt.Slice(4, 4)); - header.avgBytesPerSec = BinaryPrimitives.ReadUInt32LittleEndian(fmt.Slice(8, 4)); - header.blockAlign = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(12, 2)); - header.bitsPerSample = BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(14, 2)); - header.cbExtraData = chunkSize >= 18 ? BinaryPrimitives.ReadUInt16LittleEndian(fmt.Slice(16, 2)) : (ushort)0; - haveFormat = true; - } - else if (chunkId == "data") - { - samples = source.Slice(body, (int)chunkSize).ToArray(); - header.cbDataSize = chunkSize; - } - - // Chunks are word-aligned: an odd-sized chunk is followed by a pad byte. - offset = body + (int)chunkSize + ((chunkSize & 1) != 0 ? 1 : 0); - } - - // Only uncompressed PCM is representable through this surface -- the managed side - // declares WAVE_FORMAT_PCM as its sole format constant, so anything else would be - // silently misinterpreted downstream rather than merely unsupported. - return haveFormat && samples.Length > 0 && header.formatTag == WAVE_FORMAT_PCM; - } -} diff --git a/UIXrender/Subsystems/Data/DataApi.cs b/UIXrender/Subsystems/Data/DataApi.cs deleted file mode 100644 index 42e7c60..0000000 --- a/UIXrender/Subsystems/Data/DataApi.cs +++ /dev/null @@ -1,199 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Data; - -// [UnmanagedCallersOnly] exports for the SpData* family in UIX/Microsoft/Iris/OS/NativeApi.cs. -public static unsafe class DataApi -{ - private static uint OK => (uint)HRESULT.S_OK.hr; - private static uint InvalidArg => unchecked((uint)HRESULT.E_INVALIDARG.hr); - - // Mirrors NativeApi.NativeDataMappingEntry's marshaled layout: four LPWStr pointers - // interleaved with two 64-bit type handles, then a UIXVariant. Declared here rather - // than in Interop/ because nothing outside this subsystem reads it. - [StructLayout(LayoutKind.Sequential)] - public struct NativeDataMappingEntry - { - public IntPtr Source; - public IntPtr Target; - public IntPtr PropertyName; - public ulong PropertyTypeHandle; - public IntPtr PropertyTypeName; - public ulong UnderlyingCollectionTypeHandle; - public IntPtr UnderlyingCollectionTypeName; - public UIXVariant DefaultValue; - } - - // ---- provider / factory ---------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpDataProviderConstructQuery")] - public static uint SpDataProviderConstructQuery(IntPtr nativeFactory, char* providerName, ulong queryTypeHandle, ulong resultTypeHandle, ulong queryHandle, IntPtr* query) - { - if (query == null) - return InvalidArg; - - var constructed = new DataQuery(NativeString.UniToString(providerName), queryTypeHandle, resultTypeHandle, queryHandle); - *query = HandleTable.Alloc(constructed); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataProviderReportDataMapping")] - public static uint SpDataProviderReportDataMapping(IntPtr nativeCallback, char* providerName, ulong typeHandle, uint entryCount, NativeDataMappingEntry* entries) - { - if (entries == null && entryCount > 0) - return InvalidArg; - - // The callback pointer identifies the factory collecting these mappings; a caller - // that hasn't registered one yet still gets a well-defined success, matching the - // original's fire-and-forget "report" shape. - DataProviderFactory factory = HandleTable.Get(nativeCallback); - if (factory == null) - return OK; - - string provider = NativeString.UniToString(providerName); - for (uint i = 0; i < entryCount; i++) - { - NativeDataMappingEntry entry = entries[i]; - factory.ReportMapping(new DataMapping - { - ProviderName = provider, - Source = Marshal.PtrToStringUni(entry.Source), - Target = Marshal.PtrToStringUni(entry.Target), - PropertyName = Marshal.PtrToStringUni(entry.PropertyName), - PropertyTypeHandle = entry.PropertyTypeHandle, - PropertyTypeName = Marshal.PtrToStringUni(entry.PropertyTypeName), - UnderlyingCollectionTypeHandle = entry.UnderlyingCollectionTypeHandle, - UnderlyingCollectionTypeName = Marshal.PtrToStringUni(entry.UnderlyingCollectionTypeName), - DefaultValue = entry.DefaultValue, - }); - } - - return OK; - } - - // ---- query lifecycle ------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpDataQueryNotifyInitialized")] - public static uint SpDataQueryNotifyInitialized(IntPtr nativeQuery) - { - if (!HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - query.NotifyInitialized(); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQueryRefresh")] - public static uint SpDataQueryRefresh(IntPtr nativeQuery) - { - if (!HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - query.Refresh(); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQueryGetEnabledProperty")] - public static uint SpDataQueryGetEnabledProperty(IntPtr nativeQuery, int* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - *propertyValue = query.Enabled ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQuerySetEnabledProperty")] - public static uint SpDataQuerySetEnabledProperty(IntPtr nativeQuery, int propertyValue) - { - if (!HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - query.Enabled = propertyValue != 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQueryGetStatusProperty")] - public static uint SpDataQueryGetStatusProperty(IntPtr nativeQuery, DataProviderQueryStatus* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - *propertyValue = query.Status; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQuerySetStatusProperty")] - public static uint SpDataQuerySetStatusProperty(IntPtr nativeQuery, DataProviderQueryStatus propertyValue) - { - if (!HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - query.Status = propertyValue; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQueryGetResultProperty")] - public static uint SpDataQueryGetResultProperty(IntPtr nativeQuery, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - *propertyValue = query.Result; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataQuerySetResultProperty")] - public static uint SpDataQuerySetResultProperty(IntPtr nativeQuery, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataQuery query)) - return InvalidArg; - query.Result = *propertyValue; - return OK; - } - - // ---- base object ----------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpDataBaseObjectGetTypeHandle")] - public static void SpDataBaseObjectGetTypeHandle(IntPtr nativeQuery, ulong* typeHandle) - { - if (typeHandle == null) - return; - *typeHandle = HandleTable.TryGet(nativeQuery, out DataBaseObject obj) ? obj.TypeHandle : 0UL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataBaseObjectGetProperty")] - public static uint SpDataBaseObjectGetProperty(IntPtr nativeQuery, char* propertyName, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataBaseObject obj)) - return InvalidArg; - - *propertyValue = obj.TryGetProperty(NativeString.UniToString(propertyName), out UIXVariant value) ? value : UIXVariant.Empty; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataBaseObjectSetProperty")] - public static uint SpDataBaseObjectSetProperty(IntPtr nativeQuery, char* propertyName, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(nativeQuery, out DataBaseObject obj)) - return InvalidArg; - - obj.SetProperty(NativeString.UniToString(propertyName), *propertyValue); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataBaseObjectSetInternalHandle")] - public static uint SpDataBaseObjectSetInternalHandle(IntPtr nativeQuery, ulong frameworkQuery) - { - if (!HandleTable.TryGet(nativeQuery, out DataBaseObject obj)) - return InvalidArg; - obj.InternalHandle = frameworkQuery; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDataBaseObjectGetInternalHandle")] - public static uint SpDataBaseObjectGetInternalHandle(IntPtr nativeQuery, ulong* frameworkQuery) - { - if (frameworkQuery == null || !HandleTable.TryGet(nativeQuery, out DataBaseObject obj)) - return InvalidArg; - *frameworkQuery = obj.InternalHandle; - return OK; - } -} diff --git a/UIXrender/Subsystems/Data/DataModel.cs b/UIXrender/Subsystems/Data/DataModel.cs deleted file mode 100644 index 31a44ee..0000000 --- a/UIXrender/Subsystems/Data/DataModel.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Data; - -// Managed model behind the SpData* family (UIX/Microsoft/Iris/OS/NativeApi.cs) -- the -// native side of Iris's data-binding/query system, i.e. how a markup-declared binding -// reaches a provider's query result. -// -// Mirrors Microsoft.Iris.DataProviderQueryStatus exactly (UIX/Microsoft/Iris/DataProviderQueryStatus.cs). -public enum DataProviderQueryStatus -{ - Idle, - RequestingData, - ProcessingData, - Complete, - Error, -} - -// The base object both queries and provider-produced result objects derive from -- backs -// SpDataBaseObjectGet/SetProperty and SpDataBaseObjectGet/SetInternalHandle. -internal class DataBaseObject -{ - private readonly Dictionary _properties = new(StringComparer.Ordinal); - - public ulong TypeHandle { get; set; } - - // Opaque to us: the framework's own handle for the object this one shadows. - public ulong InternalHandle { get; set; } - - public bool TryGetProperty(string name, out UIXVariant value) - { - if (name == null) - { - value = default; - return false; - } - return _properties.TryGetValue(name, out value); - } - - public void SetProperty(string name, UIXVariant value) - { - if (name != null) - _properties[name] = value; - } -} - -internal sealed class DataQuery : DataBaseObject -{ - public DataQuery(string providerName, ulong queryTypeHandle, ulong resultTypeHandle, ulong queryHandle) - { - ProviderName = providerName; - QueryTypeHandle = queryTypeHandle; - ResultTypeHandle = resultTypeHandle; - InternalHandle = queryHandle; - TypeHandle = queryTypeHandle; - } - - public string ProviderName { get; } - public ulong QueryTypeHandle { get; } - public ulong ResultTypeHandle { get; } - - public bool Enabled { get; set; } - public DataProviderQueryStatus Status { get; set; } = DataProviderQueryStatus.Idle; - public UIXVariant Result { get; set; } - public bool Initialized { get; private set; } - - public void NotifyInitialized() => Initialized = true; - - // A refresh on a disabled query is a no-op in the original's vocabulary (Enabled is - // exactly the "should this query run" switch), so status only advances when enabled. - // Nothing here fetches real data -- there is no provider implementation on this side - // of the boundary; the provider lives in managed framework code and pushes results - // back in through SpDataQuerySetResultProperty. - public void Refresh() - { - if (!Enabled) - { - Status = DataProviderQueryStatus.Idle; - return; - } - - Status = DataProviderQueryStatus.RequestingData; - } -} - -// Backs SpDataProviderConstructQuery/SpDataProviderReportDataMapping. -internal sealed class DataProviderFactory -{ - private readonly List _mappings = new(); - - public IReadOnlyList Mappings => _mappings; - - public void ReportMapping(DataMapping mapping) => _mappings.Add(mapping); -} - -// Managed projection of NativeApi.NativeDataMappingEntry -- read as a struct of marshaled -// strings/handles at the boundary, kept as an ordinary object here. -internal sealed class DataMapping -{ - public string ProviderName { get; init; } - public string Source { get; init; } - public string Target { get; init; } - public string PropertyName { get; init; } - public ulong PropertyTypeHandle { get; init; } - public string PropertyTypeName { get; init; } - public ulong UnderlyingCollectionTypeHandle { get; init; } - public string UnderlyingCollectionTypeName { get; init; } - public UIXVariant DefaultValue { get; init; } -} diff --git a/UIXrender/Subsystems/Graphics/FormApi.cs b/UIXrender/Subsystems/Graphics/FormApi.cs deleted file mode 100644 index 03af652..0000000 --- a/UIXrender/Subsystems/Graphics/FormApi.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Graphics; - -// [UnmanagedCallersOnly] exports matching UIX.RenderApi/Microsoft/Iris/Render/Internal/FormApi.cs. -// -// Real, and legitimately a no-op pair rather than a stub: these existed so the original -// C++ code could call GdiplusStartup/GdiplusShutdown before using GDI+ for image decoding -// and text rasterization. This reimplementation decodes images with StbImageSharp (pure -// managed, see logs/UIXrender/FullSurface.md, decision 3) and never initialises GDI+ at -// all, so there is genuinely nothing to start up -- reporting success is the correct -// answer, not a deferral. Recorded explicitly so a future reader doesn't mistake these -// for unimplemented. -public static class FormApi -{ - [UnmanagedCallersOnly(EntryPoint = "SpGdiplusInit")] - public static HRESULT SpGdiplusInit() => HRESULT.S_OK; - - [UnmanagedCallersOnly(EntryPoint = "SpGdiplusUninit")] - public static HRESULT SpGdiplusUninit() => HRESULT.S_OK; -} diff --git a/UIXrender/Subsystems/Lists/UIXList.cs b/UIXrender/Subsystems/Lists/UIXList.cs deleted file mode 100644 index 3028d77..0000000 --- a/UIXrender/Subsystems/Lists/UIXList.cs +++ /dev/null @@ -1,130 +0,0 @@ -using System; -using System.Collections.Generic; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Lists; - -// The managed model behind the SpUIXList* family (UIX/Microsoft/Iris/OS/NativeApi.cs) -- -// the native backing store for a data-bound/virtualized ListBox. -// -// "Virtualized" here means the list distinguishes items that are resident from items that -// still need fetching: IsItemAvailable/FetchSlowData/WantSlowDataRequests are the -// original surface's vocabulary for that, and SlowDataAcquireComplete is how the native -// side told the managed listener an item had arrived. This implementation keeps that -// distinction real (an availability flag per slot) rather than pretending every item is -// always resident, because the managed ListBox's scrolling behavior depends on it. -internal sealed class UIXList -{ - private readonly List _items = new(); - private readonly List _available = new(); - private readonly List _listeners = new(); - - public int Count => _items.Count; - - public bool WantSlowDataRequests { get; set; } - - public IReadOnlyList Listeners => _listeners; - - public void RegisterListener(IntPtr listener) - { - if (listener != IntPtr.Zero && !_listeners.Contains(listener)) - _listeners.Add(listener); - } - - public void UnregisterListener(IntPtr listener) => _listeners.Remove(listener); - - public int Add(UIXVariant item) - { - _items.Add(item); - _available.Add(true); - return _items.Count; - } - - public bool Insert(int index, UIXVariant item) - { - if ((uint)index > (uint)_items.Count) - return false; - _items.Insert(index, item); - _available.Insert(index, true); - return true; - } - - public bool RemoveAt(int index) - { - if ((uint)index >= (uint)_items.Count) - return false; - _items.RemoveAt(index); - _available.RemoveAt(index); - return true; - } - - public bool Remove(UIXVariant item) - { - int index = IndexOf(item); - return index >= 0 && RemoveAt(index); - } - - public int IndexOf(UIXVariant item) - { - for (int i = 0; i < _items.Count; i++) - { - if (_items[i].type == item.type && _items[i].union == item.union) - return i; - } - return -1; - } - - public void Clear() - { - _items.Clear(); - _available.Clear(); - } - - public bool TryGet(int index, out UIXVariant item) - { - if ((uint)index >= (uint)_items.Count) - { - item = default; - return false; - } - item = _items[index]; - return true; - } - - public bool TrySet(int index, UIXVariant item) - { - if ((uint)index >= (uint)_items.Count) - return false; - _items[index] = item; - _available[index] = true; - return true; - } - - public bool Move(int oldIndex, int newIndex) - { - if ((uint)oldIndex >= (uint)_items.Count || (uint)newIndex >= (uint)_items.Count) - return false; - - UIXVariant item = _items[oldIndex]; - bool available = _available[oldIndex]; - _items.RemoveAt(oldIndex); - _available.RemoveAt(oldIndex); - _items.Insert(newIndex, item); - _available.Insert(newIndex, available); - return true; - } - - public bool IsItemAvailable(int index) => (uint)index < (uint)_available.Count && _available[index]; - - // Marks a slot as pending. The managed listener is told the data arrived via - // SlowDataAcquireComplete; since nothing in this reimplementation performs a real - // asynchronous fetch yet, the item is immediately considered resident again -- the - // notification still fires, so the listener's state machine is exercised correctly. - public bool FetchSlowData(int index) - { - if ((uint)index >= (uint)_available.Count) - return false; - _available[index] = true; - return true; - } -} diff --git a/UIXrender/Subsystems/Lists/UIXListApi.cs b/UIXrender/Subsystems/Lists/UIXListApi.cs deleted file mode 100644 index d0f629e..0000000 --- a/UIXrender/Subsystems/Lists/UIXListApi.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Com; - -namespace Microsoft.Iris.Render.Subsystems.Lists; - -// [UnmanagedCallersOnly] exports for the SpUIXList* family in -// UIX/Microsoft/Iris/OS/NativeApi.cs. -// -// The `type` argument passed to IUIXListCallbacks.ListChanged is NOT guessed: the managed -// receiver (UIX/Microsoft/Iris/CodeModel/Cpp/DllProxyList.cs, ListChanged) casts it -// directly to Microsoft.Iris.Data.UIListContentsChangeType, so the numbering below is -// that enum's, read off the decompiled source. -public static unsafe class UIXListApi -{ - private enum ChangeType - { - Add = 0, - AddRange = 1, - Remove = 2, - Move = 3, - Insert = 4, - InsertRange = 5, - Clear = 6, - Modified = 7, - Reset = 8, - } - - private static uint OK => (uint)HRESULT.S_OK.hr; - private static uint InvalidArg => unchecked((uint)HRESULT.E_INVALIDARG.hr); - private static uint Fail => unchecked((uint)HRESULT.E_FAIL.hr); - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListRegisterCallbacks")] - public static uint SpUIXListRegisterCallbacks(IntPtr nativeList, IntPtr listener) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - - // The list holds the callback for as long as it's registered, so it takes a - // reference -- otherwise the caller could release its own last reference and - // leave us calling into freed memory on the next change notification. - ComVtable.AddRef(listener); - list.RegisterListener(listener); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListUnregisterCallbacks")] - public static uint SpUIXListUnregisterCallbacks(IntPtr nativeList, IntPtr listener) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - - list.UnregisterListener(listener); - ComVtable.Release(listener); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListAdd")] - public static uint SpUIXListAdd(IntPtr nativeList, UIXVariant* item, int* count) - { - if (item == null || count == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - - *count = list.Add(*item); - NotifyListChanged(list, ChangeType.Add, -1, list.Count - 1, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListInsert")] - public static uint SpUIXListInsert(IntPtr nativeList, int index, UIXVariant* item) - { - if (item == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.Insert(index, *item)) - return Fail; - - NotifyListChanged(list, ChangeType.Insert, -1, index, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListRemove")] - public static uint SpUIXListRemove(IntPtr nativeList, UIXVariant* item) - { - if (item == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - - int index = list.IndexOf(*item); - if (index < 0 || !list.RemoveAt(index)) - return Fail; - - NotifyListChanged(list, ChangeType.Remove, index, -1, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListRemoveAt")] - public static uint SpUIXListRemoveAt(IntPtr nativeList, int index) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.RemoveAt(index)) - return Fail; - - NotifyListChanged(list, ChangeType.Remove, index, -1, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListMove")] - public static uint SpUIXListMove(IntPtr nativeList, int oldIndex, int newIndex) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.Move(oldIndex, newIndex)) - return Fail; - - NotifyListChanged(list, ChangeType.Move, oldIndex, newIndex, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListClear")] - public static uint SpUIXListClear(IntPtr nativeList) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - - int previousCount = list.Count; - list.Clear(); - NotifyListChanged(list, ChangeType.Clear, -1, -1, previousCount); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListGetItem")] - public static uint SpUIXListGetItem(IntPtr nativeList, int index, UIXVariant* item) - { - if (item == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.TryGet(index, out UIXVariant value)) - return Fail; - - *item = value; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListSetItem")] - public static uint SpUIXListSetItem(IntPtr nativeList, int index, UIXVariant* item) - { - if (item == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.TrySet(index, *item)) - return Fail; - - NotifyListChanged(list, ChangeType.Modified, index, index, 1); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListGetCount")] - public static uint SpUIXListGetCount(IntPtr nativeList, int* count) - { - if (count == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - *count = list.Count; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListIndexOf")] - public static uint SpUIXListIndexOf(IntPtr nativeList, UIXVariant* item, int* index) - { - if (item == null || index == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - *index = list.IndexOf(*item); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListIsItemAvailable")] - public static uint SpUIXListIsItemAvailable(IntPtr nativeList, int index, int* isAvailable) - { - if (isAvailable == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - *isAvailable = list.IsItemAvailable(index) ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListFetchSlowData")] - public static uint SpUIXListFetchSlowData(IntPtr nativeList, int index) - { - if (!HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - if (!list.FetchSlowData(index)) - return Fail; - - NotifySlowDataAcquireComplete(list, index); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListWantSlowDataRequests")] - public static uint SpUIXListWantSlowDataRequests(IntPtr nativeList, int* wantSlowDataRequests) - { - if (wantSlowDataRequests == null || !HandleTable.TryGet(nativeList, out UIXList list)) - return InvalidArg; - *wantSlowDataRequests = list.WantSlowDataRequests ? 1 : 0; - return OK; - } - - // Visual create/release notifications are the managed side telling the store which - // slots are on screen. Nothing is evicted on release yet (this store keeps everything - // resident), but the slot is validated so a bad index is reported rather than ignored. - [UnmanagedCallersOnly(EntryPoint = "SpUIXListNotifyVisualsCreated")] - public static uint SpUIXListNotifyVisualsCreated(IntPtr nativeList, int index) => - HandleTable.TryGet(nativeList, out UIXList list) && (uint)index < (uint)list.Count ? OK : InvalidArg; - - [UnmanagedCallersOnly(EntryPoint = "SpUIXListNotifyVisualsReleased")] - public static uint SpUIXListNotifyVisualsReleased(IntPtr nativeList, int index) => - HandleTable.TryGet(nativeList, out UIXList list) && (uint)index < (uint)list.Count ? OK : InvalidArg; - - // ---- listener dispatch ----------------------------------------------------------- - - private static void NotifyListChanged(UIXList list, ChangeType type, int oldIndex, int newIndex, int count) - { - foreach (IntPtr listener in list.Listeners) - { - void* fn = ComVtable.Slot(listener, ComVtable.FirstMethodSlot + 0); // ListChanged - if (fn != null) - ((delegate* unmanaged)fn)(listener, (int)type, oldIndex, newIndex, count); - } - } - - private static void NotifySlowDataAcquireComplete(UIXList list, int index) - { - foreach (IntPtr listener in list.Listeners) - { - void* fn = ComVtable.Slot(listener, ComVtable.FirstMethodSlot + 1); // SlowDataAcquireComplete - if (fn != null) - ((delegate* unmanaged)fn)(listener, index); - } - } -} diff --git a/UIXrender/Subsystems/Memory/MemoryApi.cs b/UIXrender/Subsystems/Memory/MemoryApi.cs deleted file mode 100644 index a7fcee6..0000000 --- a/UIXrender/Subsystems/Memory/MemoryApi.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Render.Engine; - -namespace Microsoft.Iris.Render.Subsystems.Memory; - -// [UnmanagedCallersOnly] exports matching SpMemAlloc/SpMemFree/SpFreeDib -// (UIX/Microsoft/Iris/OS/NativeApi.cs). Real logic: plain unmanaged heap allocation, no -// stubbing needed -- there's nothing platform-specific about a heap allocator. -public static unsafe class MemoryApi -{ - // `bool zeroMemory` in the original DllImport has no [MarshalAs] override, so the CLR - // marshals it as the default 4-byte Win32 BOOL, not 1 byte -- `int` here, not `byte`. - [UnmanagedCallersOnly(EntryPoint = "SpMemAlloc")] - public static IntPtr SpMemAlloc(uint cb, int zeroMemory) - { - if (cb == 0) - return IntPtr.Zero; - - void* p = zeroMemory != 0 ? NativeMemory.AllocZeroed(cb) : NativeMemory.Alloc(cb); - return (IntPtr)p; - } - - [UnmanagedCallersOnly(EntryPoint = "SpMemFree")] - public static void SpMemFree(IntPtr pv) - { - if (pv != IntPtr.Zero) - NativeMemory.Free((void*)pv); - } - - // Frees a text bitmap produced by SpRichTextRasterize -- the only "DIB" this - // reimplementation hands out. The handle wraps an unmanaged ARGB buffer (TextBitmap), - // disposed here. See logs/UIXrender/Rendering.md. - [UnmanagedCallersOnly(EntryPoint = "SpFreeDib")] - public static void SpFreeDib(IntPtr hdib) => HandleTable.Free(hdib); -} diff --git a/UIXrender/Subsystems/Os/DownloadApi.cs b/UIXrender/Subsystems/Os/DownloadApi.cs deleted file mode 100644 index 5bb6020..0000000 --- a/UIXrender/Subsystems/Os/DownloadApi.cs +++ /dev/null @@ -1,145 +0,0 @@ -using System; -using System.IO; -using System.Net.Http; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// [UnmanagedCallersOnly] exports for the download/HTTP family in -// UIX/Microsoft/Iris/OS/NativeApi.cs. -// -// Real and cross-platform: HttpClient for the network path, FileStream for the local one. -// Both are genuinely asynchronous like the original (the managed caller passes a -// DownloadCompleteHandler and gets called back on completion), and both hand the -// completed bytes over as an unmanaged buffer the caller reads via SpDownloadGetBuffer -// and releases via SpDownloadClose. -// -// The error codes are not invented -- NativeApi.cs declares them as public constants: -// DOWNLOAD_ERROR_NONE = 0, DOWNLOAD_ERROR_GENERALFAILURE = -1, -// HTTPDOWNLOAD_ERROR_INVALIDURI = 1, HTTPDOWNLOAD_ERROR_HOSTCONNECTIONFAILED = 2. -// -// Note the class is not `unsafe` as a whole: C# forbids `await` inside an unsafe context, -// so the async download body lives in ordinary code and only the final callback -// invocation (which needs a function pointer) is marked unsafe. -public static class DownloadApi -{ - private const int DOWNLOAD_ERROR_NONE = 0; - private const int DOWNLOAD_ERROR_GENERALFAILURE = -1; - private const int HTTPDOWNLOAD_ERROR_INVALIDURI = 1; - private const int HTTPDOWNLOAD_ERROR_HOSTCONNECTIONFAILED = 2; - - private static readonly Lazy s_httpClient = new(() => new HttpClient()); - private static int s_httpStarted; - - private sealed class Download : IDisposable - { - public IntPtr Buffer; - public uint Length; - - public void Dispose() - { - if (Buffer != IntPtr.Zero) - { - Marshal.FreeHGlobal(Buffer); - Buffer = IntPtr.Zero; - } - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpHttpStartup")] - public static void SpHttpStartup() => Interlocked.Exchange(ref s_httpStarted, 1); - - [UnmanagedCallersOnly(EntryPoint = "SpHttpShutdown")] - public static void SpHttpShutdown() => Interlocked.Exchange(ref s_httpStarted, 0); - - // HttpClient manages its own connection pool and exposes no proxy-cache flush, so - // there is nothing to invalidate here. Kept as a real (empty) implementation rather - // than a failure return: the original's callers treat this as advisory. - [UnmanagedCallersOnly(EntryPoint = "SpHttpFlushProxyCache")] - public static void SpHttpFlushProxyCache() { } - - [UnmanagedCallersOnly(EntryPoint = "SpFileDownload")] - public static unsafe uint SpFileDownload(char* path, IntPtr handler, IntPtr context, IntPtr* handle) - { - if (handle == null) - return unchecked((uint)DOWNLOAD_ERROR_GENERALFAILURE); - - string filePath = NativeString.UniToString(path); - var download = new Download(); - IntPtr downloadHandle = HandleTable.Alloc(download); - *handle = downloadHandle; - - RunDownload(() => File.ReadAllBytesAsync(filePath), download, downloadHandle, handler, context, static _ => DOWNLOAD_ERROR_GENERALFAILURE); - return DOWNLOAD_ERROR_NONE; - } - - [UnmanagedCallersOnly(EntryPoint = "SpHttpDownload")] - public static unsafe uint SpHttpDownload(char* url, IntPtr handler, IntPtr context, IntPtr* handle) - { - if (handle == null) - return unchecked((uint)DOWNLOAD_ERROR_GENERALFAILURE); - - *handle = IntPtr.Zero; - string address = NativeString.UniToString(url); - if (!Uri.TryCreate(address, UriKind.Absolute, out Uri uri)) - return HTTPDOWNLOAD_ERROR_INVALIDURI; - - var download = new Download(); - IntPtr downloadHandle = HandleTable.Alloc(download); - *handle = downloadHandle; - - RunDownload(() => s_httpClient.Value.GetByteArrayAsync(uri), download, downloadHandle, handler, context, - static e => e is HttpRequestException ? HTTPDOWNLOAD_ERROR_HOSTCONNECTIONFAILED : DOWNLOAD_ERROR_GENERALFAILURE); - - return DOWNLOAD_ERROR_NONE; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDownloadGetBuffer")] - public static IntPtr SpDownloadGetBuffer(IntPtr handle) => - HandleTable.TryGet(handle, out Download download) ? download.Buffer : IntPtr.Zero; - - [UnmanagedCallersOnly(EntryPoint = "SpDownloadClose")] - public static uint SpDownloadClose(IntPtr handle) - { - HandleTable.Free(handle); - return DOWNLOAD_ERROR_NONE; - } - - // Shared completion path for both download flavours: run the fetch off-thread, copy - // the result into an unmanaged buffer the caller can read, then invoke the caller's - // DownloadCompleteHandler(handle, error, length, context). - private static void RunDownload(Func> fetch, Download download, IntPtr downloadHandle, IntPtr handler, IntPtr context, Func classify) - { - _ = Task.Run(async () => - { - int error = DOWNLOAD_ERROR_NONE; - try - { - byte[] bytes = await fetch().ConfigureAwait(false); - IntPtr buffer = Marshal.AllocHGlobal(bytes.Length); - Marshal.Copy(bytes, 0, buffer, bytes.Length); - download.Buffer = buffer; - download.Length = (uint)bytes.Length; - } - catch (Exception e) - { - error = classify(e); - } - - InvokeCompletionHandler(handler, downloadHandle, error, download.Length, context); - }); - } - - private static unsafe void InvokeCompletionHandler(IntPtr handler, IntPtr downloadHandle, int error, uint length, IntPtr context) - { - if (handler == IntPtr.Zero) - return; - - var callback = (delegate* unmanaged)handler; - callback(downloadHandle, error, length, context); - } -} diff --git a/UIXrender/Subsystems/Os/MarshalApi.cs b/UIXrender/Subsystems/Os/MarshalApi.cs deleted file mode 100644 index de2211a..0000000 --- a/UIXrender/Subsystems/Os/MarshalApi.cs +++ /dev/null @@ -1,144 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// [UnmanagedCallersOnly] exports for the string/image marshaling helpers and the native -// services registration in UIX/Microsoft/Iris/OS/NativeApi.cs. -// -// The model here follows the original's vocabulary exactly: a "handle" (ulong) is the -// *framework's* identifier for a managed string/image, and a "native string"/"native -// image" is an object on this side that shadows it. Converting between them therefore -// requires calling back into the framework through the registered IRawUIXServices -- -// which is what makes SpCreateNativeString able to produce real text (via PinString) -// rather than an empty placeholder. -public static unsafe class MarshalApi -{ - internal sealed class NativeStringObject(string value, ulong handle) - { - public string Value { get; } = value; - public ulong Handle { get; } = handle; - } - - internal sealed class NativeImageObject(string source, ulong handle) - { - public string Source { get; } = source; - public ulong Handle { get; } = handle; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRegisterNativeServicesCallbacks")] - public static uint SpRegisterNativeServicesCallbacks(IntPtr rawServices) - { - NativeServices.Register(rawServices); - return (uint)HRESULT.S_OK.hr; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUnregisterNativeServicesCallbacks")] - public static void SpUnregisterNativeServicesCallbacks() => NativeServices.Unregister(); - - // ---- strings --------------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpGetStringHandle")] - public static void SpGetStringHandle(IntPtr nativeString, ulong* handle) - { - if (handle == null) - return; - *handle = HandleTable.TryGet(nativeString, out NativeStringObject value) ? value.Handle : 0UL; - } - - // "Convert to managed" and "get the handle" are the same operation here: the native - // string was created from a framework handle, so it already knows the managed - // identity it shadows. Kept as two exports because the managed side declares two -- - // and the body is duplicated rather than delegated because an [UnmanagedCallersOnly] - // method cannot be called from C# at all (CS8901). - [UnmanagedCallersOnly(EntryPoint = "SpConvertStringToManaged")] - public static void SpConvertStringToManaged(IntPtr nativeString, ulong* handle) - { - if (handle == null) - return; - *handle = HandleTable.TryGet(nativeString, out NativeStringObject value) ? value.Handle : 0UL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpCreateNativeString")] - public static int SpCreateNativeString(ulong handle, int length, IntPtr* nativeString) - { - if (nativeString == null) - return 0; - - *nativeString = IntPtr.Zero; - - // Read the framework's characters through the registered services callback. Without - // a registration there's no way to reach the text, so the creation genuinely fails - // rather than silently producing an empty string that would look like valid data. - if (!NativeServices.IsRegistered) - return 0; - - char* pinned = NativeServices.PinString(handle); - if (pinned == null) - return 0; - - try - { - string value = length > 0 ? new string(pinned, 0, length) : NativeString.UniToString(pinned); - *nativeString = HandleTable.Alloc(new NativeStringObject(value, handle)); - return 1; - } - finally - { - NativeServices.UnpinString(handle); - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpCopyString")] - public static void SpCopyString(char* source, char* destination, uint length) - { - if (source == null || destination == null || length == 0) - return; - - // Copies at most `length` characters and always terminates, so a caller-sized - // buffer can't be overrun by a longer source. - uint i = 0; - for (; i < length - 1 && source[i] != '\0'; i++) - destination[i] = source[i]; - destination[i] = '\0'; - } - - // ---- images ---------------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpGetImageHandle")] - public static void SpGetImageHandle(IntPtr nativeImage, ulong* handle) - { - if (handle == null) - return; - *handle = HandleTable.TryGet(nativeImage, out NativeImageObject image) ? image.Handle : 0UL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpConvertImageToManaged")] - public static HRESULT SpConvertImageToManaged(IntPtr nativeImage, ulong* handle) - { - if (handle == null) - return HRESULT.E_INVALIDARG; - - if (!HandleTable.TryGet(nativeImage, out NativeImageObject image)) - { - *handle = 0UL; - return HRESULT.E_INVALIDARG; - } - - *handle = image.Handle; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpCreateNativeImage")] - public static HRESULT SpCreateNativeImage(ulong handle, char* source, IntPtr* nativeImage) - { - if (nativeImage == null) - return HRESULT.E_INVALIDARG; - - *nativeImage = HandleTable.Alloc(new NativeImageObject(NativeString.UniToString(source), handle)); - return HRESULT.S_OK; - } -} diff --git a/UIXrender/Subsystems/Os/MessagePump.cs b/UIXrender/Subsystems/Os/MessagePump.cs deleted file mode 100644 index 3d98476..0000000 --- a/UIXrender/Subsystems/Os/MessagePump.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Threading; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// The render engine's message pump, backing SpPeekMessage/SpWaitMessage/SpInvoke. -// Backend-agnostic: a per-process message queue plus a wait handle -- no windowing or GPU -// dependency. This is exactly and only what the render loop uses the pump for (verified -// from UIX.RenderApi/.../Internal/RenderEngine.cs): -// -// WaitForWork(timeout) -> Wait(timeout): block until work is posted or the timeout. -// ProcessNativeEvents() -> Peek(): drain queued work, report whether any ran. -// InterThreadWake() -> PostWake(): unblock a blocked Wait from another thread. -// -// Deferred work (an async SpInvoke, a deferred IME message) is posted as an Action and -// runs on the pump-owning thread the next time it Peeks -- which is the whole point of a -// "deferred invoke": marshal the call onto the render thread instead of running it on the -// caller's. -// -// A pluggable IWindowMessageSource lets a host that owns an OS window (or a separate, -// optional windowing backend that never ships in UIXrender core) feed real window/input -// messages into the pump during Peek, without this file referencing any windowing library. -internal static class MessagePump -{ - private static readonly ConcurrentQueue s_queue = new(); - private static readonly ManualResetEventSlim s_wake = new(false); - private static volatile IWindowMessageSource s_windowSource; - - public static void SetWindowSource(IWindowMessageSource source) => s_windowSource = source; - - // Enqueue work to run on the pump thread, and wake a blocked Wait. - public static void Post(Action work) - { - if (work != null) - s_queue.Enqueue(work); - s_wake.Set(); - } - - // Wake a blocked Wait without queuing work (InterThreadWake). - public static void PostWake() => s_wake.Set(); - - // Drains and runs all queued work on the calling (pump-owning) thread, letting a - // registered window source inject OS messages first. Returns true if any work ran. - public static bool Peek() - { - bool didWork = false; - - s_windowSource?.Pump(); - - while (s_queue.TryDequeue(out Action work)) - { - work?.Invoke(); - didWork = true; - } - - // Reset only when the queue is genuinely empty, then re-check to close the race - // with a Post that enqueued between the drain and the reset (lost-wakeup guard). - if (s_queue.IsEmpty) - { - s_wake.Reset(); - if (!s_queue.IsEmpty) - s_wake.Set(); - } - - return didWork; - } - - // Blocks until work is pending or the timeout elapses. Returns immediately if work is - // already queued, so a Post that raced ahead of the Wait is never missed. - public static void Wait(uint timeoutMs) - { - if (!s_queue.IsEmpty) - return; - - s_wake.Wait(timeoutMs > int.MaxValue ? int.MaxValue : (int)timeoutMs); - } -} - -// The seam a windowing backend implements to feed OS window/input messages into the pump. -// Implementations translate their native events and MessagePump.Post them; Pump() is -// called once per Peek and must not block. Kept dependency-free on purpose -- UIXrender -// core references only this interface, never a concrete windowing library. -public interface IWindowMessageSource -{ - void Pump(); -} diff --git a/UIXrender/Subsystems/Os/ModuleApi.cs b/UIXrender/Subsystems/Os/ModuleApi.cs deleted file mode 100644 index 2388cdf..0000000 --- a/UIXrender/Subsystems/Os/ModuleApi.cs +++ /dev/null @@ -1,184 +0,0 @@ -using System; -using System.Diagnostics.CodeAnalysis; -using System.IO; -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Subsystems.Schema; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// [UnmanagedCallersOnly] exports for the "resource / DLL loading" family in -// UIX/Microsoft/Iris/OS/NativeApi.cs -- how markup (.uix/.uib) pulls in an assembly and -// discovers the types it can bind to. -// -// Real, and deliberately managed: SpLoadDll loads a **.NET assembly** and -// SpCreateDllLoadResultFactory projects its exported types through the schema subsystem -// (Subsystems/Schema), which is exactly what the managed caller then walks via -// SpQueryTypeCount/SpGetTypeSchema/... That keeps the whole "markup references a class by -// name" pipeline working end-to-end on any platform, instead of depending on the Win32 -// loader and the original's C++ type registry. -public static unsafe class ModuleApi -{ - private static uint OK => (uint)HRESULT.S_OK.hr; - private static uint Fail => unchecked((uint)HRESULT.E_FAIL.hr); - private static uint InvalidArg => unchecked((uint)HRESULT.E_INVALIDARG.hr); - - private sealed class LoadedModule(Assembly assembly, string location) - { - public Assembly Assembly { get; } = assembly; - public string Location { get; } = location; - } - - // Trim-unsafe by design: loading an assembly the host names at runtime is this - // export's entire purpose. Suppressed with justification rather than left warning -- - // see SchemaRegistration.FromAssembly for how markup-visible assemblies must be - // rooted by the host instead. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "SpLoadDll exists precisely to load a host-chosen assembly at runtime; the host must root it.")] - [UnmanagedCallersOnly(EntryPoint = "SpLoadDll")] - public static uint SpLoadDll(char* uri, IntPtr* moduleHandle) - { - if (moduleHandle == null) - return InvalidArg; - - *moduleHandle = IntPtr.Zero; - string path = NativeString.UniToString(uri); - if (string.IsNullOrEmpty(path)) - return InvalidArg; - - try - { - // Assembly.LoadFrom resolves alongside dependencies the same way the original - // Win32 LoadLibrary did for its neighbours. - Assembly assembly = Assembly.LoadFrom(Path.GetFullPath(path)); - *moduleHandle = HandleTable.Alloc(new LoadedModule(assembly, path)); - return OK; - } - catch (Exception e) when (e is IOException or BadImageFormatException) - { - return Fail; - } - } - - // The CLR has no assembly-unload for the default load context, so this releases our - // own handle rather than pretending the code was evicted. Documented rather than - // silently doing nothing: an unloadable AssemblyLoadContext would change observable - // type identity, which markup depends on staying stable for the process lifetime. - [UnmanagedCallersOnly(EntryPoint = "SpFreeDll")] - public static void SpFreeDll(IntPtr moduleHandle) => HandleTable.Free(moduleHandle); - - [UnmanagedCallersOnly(EntryPoint = "SpCreateDllLoadResultFactory")] - public static uint SpCreateDllLoadResultFactory(IntPtr moduleHandle, IntPtr* schemaFactory) - { - if (schemaFactory == null) - return InvalidArg; - - *schemaFactory = IntPtr.Zero; - if (!HandleTable.TryGet(moduleHandle, out LoadedModule module)) - return InvalidArg; - - try - { - *schemaFactory = HandleTable.Alloc(SchemaRegistration.FromAssembly(module.Assembly)); - return OK; - } - catch (ReflectionTypeLoadException) - { - return Fail; - } - } - - // The "qualifier" selects a sub-view of a module's schema (markup can reference one - // module under several qualified names). Nothing in this reimplementation partitions - // a module's exported types, so the load result is the whole registration -- a real - // result, and the same object the factory already produced. - [UnmanagedCallersOnly(EntryPoint = "SpCreateDllLoadResult")] - public static uint SpCreateDllLoadResult(IntPtr schemaFactory, char* qualifier, IntPtr* loadResult) - { - if (loadResult == null) - return InvalidArg; - - *loadResult = IntPtr.Zero; - if (!HandleTable.TryGet(schemaFactory, out SchemaRegistration registration)) - return InvalidArg; - - *loadResult = HandleTable.Alloc(registration); - return OK; - } - - // Advisory notification that a module's schema is going away; there are no cached - // per-module schema views to invalidate in this implementation. - [UnmanagedCallersOnly(EntryPoint = "SpSendDllSchemaUnloadNotification")] - public static void SpSendDllSchemaUnloadNotification(IntPtr moduleHandle) { } - - // Real, cross-platform: reads an embedded managed resource out of the loaded - // assembly (the .NET equivalent of a Win32 RT_RCDATA resource) into an unmanaged - // buffer the caller reads directly. `moduleBaseName` selects the assembly by simple - // name among those already loaded, matching the original's "base name" lookup. - [UnmanagedCallersOnly(EntryPoint = "SpLoadBinaryResource")] - public static int SpLoadBinaryResource(char* moduleBaseName, char* resourceName, int allowLoadAsCode, IntPtr* pBits, uint* size) - { - if (pBits == null || size == null) - return 0; - - *pBits = IntPtr.Zero; - *size = 0; - - Assembly assembly = FindAssembly(NativeString.UniToString(moduleBaseName)); - string name = NativeString.UniToString(resourceName); - if (assembly == null || string.IsNullOrEmpty(name)) - return 0; - - using Stream stream = assembly.GetManifestResourceStream(name); - if (stream == null) - return 0; - - var bytes = new byte[stream.Length]; - int read = stream.ReadAtLeast(bytes, bytes.Length, throwOnEndOfStream: false); - - IntPtr buffer = Marshal.AllocHGlobal(read); - Marshal.Copy(bytes, 0, buffer, read); - *pBits = buffer; - *size = (uint)read; - return 1; - } - - // Real, backend-agnostic: reads the embedded font resource and registers its bytes - // with the text engine's FontStore (which the CPU StbTrueType backend measures and - // rasterizes with), keyed by the resource's base name. No OS font registration - // (AddFontMemResourceEx / fontconfig) is involved -- the font lives entirely in the - // process for this reimplementation's own text rendering. See logs/UIXrender/Rendering.md. - [UnmanagedCallersOnly(EntryPoint = "SpLoadFontResource")] - public static int SpLoadFontResource(char* moduleBaseName, char* resourceName) - { - Assembly assembly = FindAssembly(NativeString.UniToString(moduleBaseName)); - string name = NativeString.UniToString(resourceName); - if (assembly == null || string.IsNullOrEmpty(name)) - return 0; - - using Stream stream = assembly.GetManifestResourceStream(name); - if (stream == null) - return 0; - - var bytes = new byte[stream.Length]; - stream.ReadExactly(bytes); - - string family = Path.GetFileNameWithoutExtension(name); - return Subsystems.Text.FontStore.Register(family, bytes) ? 1 : 0; - } - - private static Assembly FindAssembly(string baseName) - { - if (string.IsNullOrEmpty(baseName)) - return null; - - foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies()) - { - if (string.Equals(assembly.GetName().Name, baseName, StringComparison.OrdinalIgnoreCase)) - return assembly; - } - return null; - } -} diff --git a/UIXrender/Subsystems/Os/NativeServices.cs b/UIXrender/Subsystems/Os/NativeServices.cs deleted file mode 100644 index ad5e05e..0000000 --- a/UIXrender/Subsystems/Os/NativeServices.cs +++ /dev/null @@ -1,111 +0,0 @@ -using System; -using Microsoft.Iris.Render.Interop.Com; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// The registered IRawUIXServices callback -- how UIXrender calls *back into* the managed -// framework (UIX.dll). Registered via SpRegisterNativeServicesCallbacks. -// -// Slot numbers below are IRawUIXServices' declaration order -// (UIX/Microsoft/Iris/OS/IRawUIXServices.cs) offset by the three IUnknown slots. They are -// read off that file, not guessed; the interface is -// [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)], so there are no IDispatch slots -// in between. -internal static unsafe class NativeServices -{ - private const int SlotCrash = ComVtable.FirstMethodSlot + 0; - private const int SlotNotifyChangeForObject = ComVtable.FirstMethodSlot + 1; - private const int SlotAllocateString = ComVtable.FirstMethodSlot + 2; - private const int SlotCopyString = ComVtable.FirstMethodSlot + 3; - private const int SlotPinString = ComVtable.FirstMethodSlot + 4; - private const int SlotUnpinString = ComVtable.FirstMethodSlot + 5; - private const int SlotReleaseString = ComVtable.FirstMethodSlot + 6; - private const int SlotAllocateImageFromUri = ComVtable.FirstMethodSlot + 7; - private const int SlotAllocateImageFromBits = ComVtable.FirstMethodSlot + 8; - private const int SlotRemoveCachedImage = ComVtable.FirstMethodSlot + 9; - private const int SlotReleaseImage = ComVtable.FirstMethodSlot + 10; - private const int SlotRegisterDataProvider = ComVtable.FirstMethodSlot + 11; - private const int SlotGetDataMapping = ComVtable.FirstMethodSlot + 12; - private const int SlotNotifyChangeForDataObject = ComVtable.FirstMethodSlot + 13; - private const int SlotGetAppWindowHandle = ComVtable.FirstMethodSlot + 14; - private const int SlotReportError = ComVtable.FirstMethodSlot + 15; - private const int SlotLowPriorityDeferredInvoke = ComVtable.FirstMethodSlot + 16; - - private static IntPtr s_services; - - public static bool IsRegistered => s_services != IntPtr.Zero; - - public static void Register(IntPtr services) - { - Unregister(); - if (services != IntPtr.Zero) - { - ComVtable.AddRef(services); - s_services = services; - } - } - - public static void Unregister() - { - if (s_services != IntPtr.Zero) - { - ComVtable.Release(s_services); - s_services = IntPtr.Zero; - } - } - - // Pins the framework's string for the handle and returns a pointer to its characters. - // This is what makes SpCreateNativeString able to materialise real text rather than an - // empty placeholder: the text lives on the managed side, and this is the only way to - // read it. - public static char* PinString(ulong handle) - { - void* fn = ComVtable.Slot(s_services, SlotPinString); - return fn == null ? null : ((delegate* unmanaged)fn)(s_services, handle); - } - - public static void UnpinString(ulong handle) - { - void* fn = ComVtable.Slot(s_services, SlotUnpinString); - if (fn != null) - ((delegate* unmanaged)fn)(s_services, handle); - } - - public static void ReleaseString(ulong handle) - { - void* fn = ComVtable.Slot(s_services, SlotReleaseString); - if (fn != null) - ((delegate* unmanaged)fn)(s_services, handle); - } - - public static ulong AllocateString(char* value, out int length) - { - length = 0; - void* fn = ComVtable.Slot(s_services, SlotAllocateString); - if (fn == null) - return 0; - - fixed (int* pLength = &length) - return ((delegate* unmanaged)fn)(s_services, value, pLength); - } - - public static void ReleaseImage(ulong handle) - { - void* fn = ComVtable.Slot(s_services, SlotReleaseImage); - if (fn != null) - ((delegate* unmanaged)fn)(s_services, handle); - } - - public static void ReportError(bool isWarning, char* message) - { - void* fn = ComVtable.Slot(s_services, SlotReportError); - if (fn != null) - ((delegate* unmanaged)fn)(s_services, isWarning ? 1 : 0, message); - } - - public static IntPtr GetAppWindowHandle() - { - void* fn = ComVtable.Slot(s_services, SlotGetAppWindowHandle); - return fn == null ? IntPtr.Zero : ((delegate* unmanaged)fn)(s_services); - } -} diff --git a/UIXrender/Subsystems/Os/SystemApi.cs b/UIXrender/Subsystems/Os/SystemApi.cs deleted file mode 100644 index cc4ce59..0000000 --- a/UIXrender/Subsystems/Os/SystemApi.cs +++ /dev/null @@ -1,194 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// [UnmanagedCallersOnly] exports for the remaining miscellaneous OS entry points in -// UIX/Microsoft/Iris/OS/NativeApi.cs: DPI, mouse cursor metrics, drag-and-drop, the -// notification window, IME plumbing, registry change notification, and the deferred -// invoke helper. -// -// These are OS APIs, not graphics APIs, so per CLAUDE.md they are implemented for real on -// Windows behind `#if WINDOWS` with a documented fallback + TODO elsewhere -- rather than -// pulling in a windowing toolkit (see logs/UIXrender/FullSurface.md, decision 2, for why -// Silk.NET.Windowing/Input are the wrong tool for a library loaded into a host that -// already owns its window and pump). -public static unsafe class SystemApi -{ - // The Win32 "default" logical DPI. Every Iris layout calculation is relative to this, - // so it is the correct neutral answer where the real value can't be queried -- not a - // placeholder number. - private const int DefaultDpi = 96; - - private static uint OK => (uint)HRESULT.S_OK.hr; - - [UnmanagedCallersOnly(EntryPoint = "SpGetDpi")] - public static int SpGetDpi() - { -#if WINDOWS - return (int)Win32.GetDpiForSystem(); -#else - // TODO: query the platform's scale factor (Xft.dpi / GSettings text-scaling-factor - // on X11, wl_output scale on Wayland) once this project has a display abstraction. - return DefaultDpi; -#endif - } - - // Reports the system cursor height and the hotspot's Y offset -- used by Iris to align - // a custom-drawn cursor with the OS one. - [UnmanagedCallersOnly(EntryPoint = "SpGetMouseCursorInfo")] - public static void SpGetMouseCursorInfo(int* height, int* hotY) - { - if (height == null || hotY == null) - return; - -#if WINDOWS - const int SM_CYCURSOR = 14; - int cursorHeight = Win32.GetSystemMetrics(SM_CYCURSOR); - *height = cursorHeight > 0 ? cursorHeight : 32; - // The hotspot of the standard arrow cursor sits at its top-left, so the Y offset - // is zero; a themed cursor can differ but Win32 exposes no metric for it without - // loading and inspecting the cursor bitmap itself. - *hotY = 0; -#else - // TODO: no cross-platform cursor-metrics abstraction available yet. 32px is the - // near-universal default cursor size on both Windows and common Linux themes. - *height = 32; - *hotY = 0; -#endif - } - - // Enumerates the file names in a shell drag-and-drop data object, invoking the - // caller's callback once per file. The data object is an IDataObject COM pointer, - // which only exists on Windows. - // TODO: wire to the platform's drag-and-drop protocol (XDND / wl_data_device) when a - // cross-platform windowing layer exists. - [UnmanagedCallersOnly(EntryPoint = "SpExtractDroppedFileNames")] - public static int SpExtractDroppedFileNames(IntPtr punk, IntPtr callback) => 0; - - // The notification window is a hidden, message-only window the original used to - // receive broadcast messages (its callback fires for NotificationType.GetObject -- - // accessibility). There's no cross-platform message-only window, and nothing in-repo - // broadcasts to it, so this registers the callback and returns a real handle rather - // than creating an OS window. Crucially it now succeeds instead of E_NOTIMPL: - // UIForm.Initialize wraps this in IFC (which throws on failure), so failing here would - // abort form initialisation. The callback is retained via a GCHandle so a future - // windowing backend can invoke it. - // TODO: invoke the callback from a real notify window once a windowing backend exists. - private static IntPtr s_notifyCallback; - - [UnmanagedCallersOnly(EntryPoint = "SpCreateNotifyWindow")] - public static HRESULT SpCreateNotifyWindow(IntPtr* handle, IntPtr callback) - { - if (handle == null) - return HRESULT.E_INVALIDARG; - - s_notifyCallback = callback; - // A non-null, non-dereferenced token handle -- the managed side only checks it for - // non-null and passes it back to SpDestroyNotifyWindow (which takes no args here). - *handle = new IntPtr(1); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpDestroyNotifyWindow")] - public static void SpDestroyNotifyWindow() => s_notifyCallback = IntPtr.Zero; - - // IME (input method editor) composition forwarding. The callbacks are held so - // registration/unregistration round-trips correctly; SpPostDeferredImeMessage now - // dispatches to them through the message pump (deferred onto the render thread), so - // this is wired end-to-end -- a windowing backend that produces composition events - // just needs to call SpPostDeferredImeMessage. - private static readonly System.Collections.Concurrent.ConcurrentDictionary s_imeCallbacks = new(); - private static uint s_nextImeToken; - - [UnmanagedCallersOnly(EntryPoint = "SpRegisterImeCallbacks")] - public static HRESULT SpRegisterImeCallbacks(IntPtr pImeCallbacks, uint* dwToken) - { - if (dwToken == null) - return HRESULT.E_INVALIDARG; - - uint token = System.Threading.Interlocked.Increment(ref s_nextImeToken); - Interop.Com.ComVtable.AddRef(pImeCallbacks); - s_imeCallbacks[token] = pImeCallbacks; - *dwToken = token; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpUnregisterImeCallbacks")] - public static HRESULT SpUnregisterImeCallbacks(uint dwToken) - { - if (!s_imeCallbacks.TryRemove(dwToken, out IntPtr callbacks)) - return HRESULT.E_INVALIDARG; - - Interop.Com.ComVtable.Release(callbacks); - return HRESULT.S_OK; - } - - // "Deferred" per its name: posts the dispatch onto the render thread's pump rather - // than fanning out synchronously on the caller's thread. Each registered IImeCallbacks - // then gets OnImeMessageReceived (its single method, hence the first vtable slot) when - // the render thread next peeks. - [UnmanagedCallersOnly(EntryPoint = "SpPostDeferredImeMessage")] - public static HRESULT SpPostDeferredImeMessage(uint message, UIntPtr wParam, UIntPtr lParam) - { - MessagePump.Post(() => DispatchIme(message, wParam, lParam)); - return HRESULT.S_OK; - } - - private static void DispatchIme(uint message, UIntPtr wParam, UIntPtr lParam) - { - foreach (IntPtr callbacks in s_imeCallbacks.Values) - { - void* fn = Interop.Com.ComVtable.Slot(callbacks, Interop.Com.ComVtable.FirstMethodSlot); - if (fn != null) - ((delegate* unmanaged)fn)(callbacks, message, wParam, lParam); - } - } - - // Registry-change notification is inherently a Windows concept (the managed callers - // are Configuration classes reading HKLM/HKCU). - [UnmanagedCallersOnly(EntryPoint = "SpRegNotifyChangeKey")] - public static HRESULT SpRegNotifyChangeKey(IntPtr hkey, char* wszPath, IntPtr callback, IntPtr* handle) - { - if (handle == null) - return HRESULT.E_INVALIDARG; - - *handle = IntPtr.Zero; -#if WINDOWS - var watcher = RegistryChangeWatcher.Create(hkey, NativeString.UniToString(wszPath), callback); - if (watcher == null) - return HRESULT.E_FAIL; - - *handle = HandleTable.Alloc(watcher); - return HRESULT.S_OK; -#else - // TODO: there is no registry on non-Windows platforms; a future settings - // abstraction (see the ZuneDBApi registry abstraction work) should back this. - return HRESULT.E_NOTIMPL; -#endif - } - - [UnmanagedCallersOnly(EntryPoint = "SpRegRevokeNotifyChangeKey")] - public static HRESULT SpRegRevokeNotifyChangeKey(IntPtr handle) - { - if (handle == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - HandleTable.Free(handle); - return HRESULT.S_OK; - } - - // Real: invokes the supplied callback immediately on the calling thread. The - // "deferred" in the name refers to the *caller* having deferred it to this point -- - // NativeApi.cs's own declaration takes the callback and its data with no scheduling - // parameters, so there is nothing to schedule against here. - [UnmanagedCallersOnly(EntryPoint = "SpCallDeferredInvokeProc")] - public static void SpCallDeferredInvokeProc(IntPtr pfnCallback, IntPtr pvCallbackData) - { - if (pfnCallback != IntPtr.Zero) - ((delegate* unmanaged)pfnCallback)(pvCallbackData); - } -} diff --git a/UIXrender/Subsystems/Os/Win32.cs b/UIXrender/Subsystems/Os/Win32.cs deleted file mode 100644 index 9255347..0000000 --- a/UIXrender/Subsystems/Os/Win32.cs +++ /dev/null @@ -1,86 +0,0 @@ -#if WINDOWS -using System; -using System.Runtime.InteropServices; -using System.Threading; - -namespace Microsoft.Iris.Render.Subsystems.Os; - -// Real Win32 entry points used by the platform-gated branches of SystemApi. These are OS -// APIs (DPI, system metrics, registry notification), not graphics APIs -- see -// logs/UIXrender/FullSurface.md, decision 2. -internal static partial class Win32 -{ - [LibraryImport("user32.dll")] - public static partial uint GetDpiForSystem(); - - [LibraryImport("user32.dll")] - public static partial int GetSystemMetrics(int nIndex); - - [LibraryImport("advapi32.dll", EntryPoint = "RegOpenKeyExW", StringMarshalling = StringMarshalling.Utf16)] - public static partial int RegOpenKeyEx(IntPtr hKey, string subKey, uint options, uint samDesired, out IntPtr result); - - [LibraryImport("advapi32.dll")] - public static partial int RegCloseKey(IntPtr hKey); - - [LibraryImport("advapi32.dll", EntryPoint = "RegNotifyChangeKeyValue")] - public static partial int RegNotifyChangeKeyValue(IntPtr hKey, [MarshalAs(UnmanagedType.Bool)] bool watchSubtree, uint notifyFilter, IntPtr hEvent, [MarshalAs(UnmanagedType.Bool)] bool asynchronous); -} - -// Backs SpRegNotifyChangeKey: opens the requested subkey, then parks a background thread -// on a real RegNotifyChangeKeyValue event, re-arming after each notification (the Win32 -// API is one-shot) and invoking the caller's RegChangeCallback each time. -internal sealed unsafe class RegistryChangeWatcher : IDisposable -{ - private const uint KEY_NOTIFY = 0x0010; - private const uint REG_NOTIFY_CHANGE_NAME = 0x00000001; - private const uint REG_NOTIFY_CHANGE_LAST_SET = 0x00000004; - - private readonly IntPtr _key; - private readonly IntPtr _callback; - private readonly ManualResetEventSlim _stopping = new(false); - private readonly AutoResetEvent _changed = new(false); - private readonly Thread _thread; - - private RegistryChangeWatcher(IntPtr key, IntPtr callback) - { - _key = key; - _callback = callback; - _thread = new Thread(Run) { IsBackground = true, Name = "UIXrender.RegistryChangeWatcher" }; - _thread.Start(); - } - - public static RegistryChangeWatcher Create(IntPtr hkey, string path, IntPtr callback) - { - if (Win32.RegOpenKeyEx(hkey, path ?? string.Empty, 0, KEY_NOTIFY, out IntPtr key) != 0) - return null; - - return new RegistryChangeWatcher(key, callback); - } - - private void Run() - { - WaitHandle[] handles = [_changed, _stopping.WaitHandle]; - - while (!_stopping.IsSet) - { - if (Win32.RegNotifyChangeKeyValue(_key, watchSubtree: true, REG_NOTIFY_CHANGE_NAME | REG_NOTIFY_CHANGE_LAST_SET, _changed.SafeWaitHandle.DangerousGetHandle(), asynchronous: true) != 0) - return; - - if (WaitHandle.WaitAny(handles) != 0 || _stopping.IsSet) - return; - - if (_callback != IntPtr.Zero) - ((delegate* unmanaged)_callback)(); - } - } - - public void Dispose() - { - _stopping.Set(); - _thread.Join(TimeSpan.FromSeconds(1)); - Win32.RegCloseKey(_key); - _changed.Dispose(); - _stopping.Dispose(); - } -} -#endif diff --git a/UIXrender/Subsystems/Remote/RemoteServerConnection.cs b/UIXrender/Subsystems/Remote/RemoteServerConnection.cs deleted file mode 100644 index 87d1934..0000000 --- a/UIXrender/Subsystems/Remote/RemoteServerConnection.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System; -using System.IO; -using System.IO.Pipes; -using System.Net; -using System.Net.Sockets; -using System.Runtime.InteropServices; -using System.Threading; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Protocol; - -namespace Microsoft.Iris.Render.Subsystems.Remote; - -// Backs the four SpRemote* exports (Protocol/EngineApi.cs) for the out-of-process -// "RemoteChannel" path. Real, working transport over .NET sockets/named pipes -- TCP and -// PIPE are genuine duplex byte streams so one connection object serves as both the -// "send" and "receive" stream handle the managed side round-trips back to us; VC is -// left unimplemented (E_NOTIMPL) since its meaning was never recovered from the -// decompiled managed code (see logs/UIXrender/Architecture.md open question #2) -- not -// guessed at further. Once connected, wire format matches the local channel: each frame -// is a length-prefixed BufferInfo + payload, dispatched into the same -// EngineService/ContextRegistry local channel uses, so remote and local contexts are -// indistinguishable to the rest of the engine. -// -// Handle model (matters for correctness -- RemoteChannel.Connect releases both stream -// handles right after ServerInit, then disposes the session later): CreateServerStreams -// hands out *two distinct* GCHandles to one connection with a refcount of 2; ServerInit -// adds a third (the session). Each SpObjectRelease / SpRemoteServerUninit frees its own -// handle and decrements; the connection's sockets are torn down exactly once, when the -// last reference goes. The receive callback is a pointer-free BufferReceivedHandler, -// exactly like RenderThread's -- the native shim adapts a raw function pointer into one, -// the managed-direct caller adapts its own delegate, and this class never sees a pointer. -internal sealed class RemoteServerConnection : IDisposable -{ - private TcpListener _tcpListener; - private NamedPipeServerStream _pipe; - private UdpClient _udp; - private IPEndPoint _udpRemote; - private Stream _stream; - private ContextID _contextId; - private Thread _readThread; - private volatile bool _running; - private int _refCount; - - public static HRESULT CreateServerStreams(TransportProtocol protocol, string sessionName, out IntPtr sendHandle, out IntPtr receiveHandle) - { - sendHandle = IntPtr.Zero; - receiveHandle = IntPtr.Zero; - - var connection = new RemoteServerConnection(); - switch (protocol) - { - case TransportProtocol.TCP: - connection._tcpListener = new TcpListener(IPAddress.Any, 0); - connection._tcpListener.Start(); - break; - case TransportProtocol.PIPE: - connection._pipe = new NamedPipeServerStream(string.IsNullOrEmpty(sessionName) ? "UIXrender" : sessionName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous); - break; - case TransportProtocol.UDP: - connection._udp = new UdpClient(0); - break; - case TransportProtocol.VC: - default: - // TODO: "VC" transport semantics were never recovered -- see - // logs/UIXrender/Architecture.md open question #2. Not guessed at. - connection.Dispose(); - return HRESULT.E_NOTIMPL; - } - - // Two distinct handles to the one duplex connection (send + receive), so releasing - // each is a separate, non-double-freeing operation. See the handle-model comment. - connection._refCount = 2; - sendHandle = GCHandle.ToIntPtr(GCHandle.Alloc(connection, GCHandleType.Normal)); - receiveHandle = GCHandle.ToIntPtr(GCHandle.Alloc(connection, GCHandleType.Normal)); - return HRESULT.S_OK; - } - - public static HRESULT WaitConnected(TransportProtocol protocol, IntPtr sendHandle) - { - if (sendHandle == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - var connection = (RemoteServerConnection)GCHandle.FromIntPtr(sendHandle).Target!; - try - { - switch (protocol) - { - case TransportProtocol.TCP: - connection._stream = connection._tcpListener.AcceptTcpClient().GetStream(); - break; - case TransportProtocol.PIPE: - connection._pipe.WaitForConnection(); - connection._stream = connection._pipe; - break; - case TransportProtocol.UDP: - // First datagram received "connects" the client, matching TCP/PIPE's - // blocking-until-a-client-attaches semantics as closely as UDP allows. - var buffer = connection._udp.Receive(ref connection._udpRemote); - connection._udp.Connect(connection._udpRemote); - connection._stream = connection._udp.Client is { } sock ? new NetworkStream(sock) : null; - break; - default: - return HRESULT.E_NOTIMPL; - } - } - catch (SocketException) - { - return HRESULT.E_FAIL; - } - catch (IOException) - { - return HRESULT.E_FAIL; - } - - return HRESULT.S_OK; - } - - // onRemoteToLocal receives frames arriving from the peer; null means "drop them" - // (RemoteChannel connects without a receive callback -- it drives the wire in one - // direction only). Pointer-free by design: the caller adapts whatever callback - // representation it has (native function pointer, managed delegate) into this shape. - public static HRESULT ServerInit(IntPtr sendHandle, ContextID context, BufferReceivedHandler onRemoteToLocal, out IntPtr pSession) - { - pSession = IntPtr.Zero; - if (sendHandle == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - var connection = (RemoteServerConnection)GCHandle.FromIntPtr(sendHandle).Target!; - if (connection._stream == null) - return HRESULT.E_FAIL; - - connection._contextId = context; - - // Local -> remote: anything sent to this context is framed onto the wire. - ContextRegistry.Register(context, connection.WriteFrame); - - connection._running = true; - connection._readThread = new Thread(() => connection.ReadLoop(onRemoteToLocal)) { IsBackground = true, Name = "UIXrender.RemoteServerConnection" }; - connection._readThread.Start(); - - Interlocked.Increment(ref connection._refCount); - pSession = GCHandle.ToIntPtr(GCHandle.Alloc(connection, GCHandleType.Normal)); - return HRESULT.S_OK; - } - - public static HRESULT ServerUninit(IntPtr pSession, bool forceShutdown, out ShutdownReason reason) - { - reason = ShutdownReason.SelfShutdown; - if (pSession == IntPtr.Zero) - return HRESULT.E_INVALIDARG; - - // Stop the reader promptly (this is the "don't wait for a graceful peer shutdown" - // meaning of forceShutdown), then drop the session's reference. - if (GCHandle.FromIntPtr(pSession).Target is RemoteServerConnection connection) - connection._running = false; - - ReleaseHandle(pSession); - return HRESULT.S_OK; - } - - // Backs the managed-direct SpObjectRelease on a stream handle: drops one reference and - // frees that handle, disposing the connection only when the last reference goes. - public static void ReleaseHandle(IntPtr handle) - { - if (handle == IntPtr.Zero) - return; - - GCHandle gc = GCHandle.FromIntPtr(handle); - var connection = gc.Target as RemoteServerConnection; - gc.Free(); - - if (connection != null && Interlocked.Decrement(ref connection._refCount) == 0) - connection.Dispose(); - } - - private unsafe void WriteFrame(ContextID source, RENDERHANDLE bufferHandle, BufferFlags flags, ReadOnlySpan data) - { - if (_stream == null) - return; - - var info = new BufferInfo - { - idContextSrc = source, - idContextDest = _contextId, - idBuffer = bufferHandle, - nFlags = flags, - cbSizeBuffer = (uint)data.Length, - }; - - Span header = stackalloc byte[sizeof(uint) + sizeof(BufferInfo)]; - BitConverter.TryWriteBytes(header, (uint)sizeof(BufferInfo) + (uint)data.Length); - fixed (byte* pHeader = header) - *(BufferInfo*)(pHeader + sizeof(uint)) = info; - - lock (this) - { - _stream.Write(header); - _stream.Write(data); - _stream.Flush(); - } - } - - private unsafe void ReadLoop(BufferReceivedHandler onRemoteToLocal) - { - var lengthBuffer = new byte[sizeof(uint)]; - var infoBuffer = new byte[sizeof(BufferInfo)]; - - try - { - while (_running) - { - if (!ReadExact(lengthBuffer)) - break; - uint totalSize = BitConverter.ToUInt32(lengthBuffer); - if (!ReadExact(infoBuffer)) - break; - - BufferInfo info; - fixed (byte* p = infoBuffer) - info = *(BufferInfo*)p; - - uint payloadSize = totalSize - (uint)sizeof(BufferInfo); - var payload = payloadSize > 0 ? new byte[payloadSize] : Array.Empty(); - if (payloadSize > 0 && !ReadExact(payload)) - break; - - onRemoteToLocal?.Invoke(info.idContextSrc, info.idBuffer, info.nFlags, payload); - } - } - catch (IOException) { } - catch (ObjectDisposedException) { } - } - - private bool ReadExact(byte[] buffer) - { - int offset = 0; - while (offset < buffer.Length) - { - int read = _stream.Read(buffer, offset, buffer.Length - offset); - if (read <= 0) - return false; - offset += read; - } - return true; - } - - public void Dispose() - { - _running = false; - ContextRegistry.Unregister(_contextId); - _stream?.Dispose(); - _tcpListener?.Stop(); - _pipe?.Dispose(); - _udp?.Dispose(); - _readThread?.Join(TimeSpan.FromSeconds(1)); - } -} diff --git a/UIXrender/Subsystems/Schema/SchemaApi.cs b/UIXrender/Subsystems/Schema/SchemaApi.cs deleted file mode 100644 index 216639f..0000000 --- a/UIXrender/Subsystems/Schema/SchemaApi.cs +++ /dev/null @@ -1,616 +0,0 @@ -#if NETCOREAPP - -using System; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Schema; - -// [UnmanagedCallersOnly] exports for the "native reflection / type-schema" family in -// UIX/Microsoft/Iris/OS/NativeApi.cs (~48 entry points). See SchemaModel.cs for why these -// project CLR types instead of the original's C++ ones. -// -// Two marshaling conventions to keep straight, both read off the original DllImport -// declarations rather than assumed: -// * every `out bool` is a 4-byte Win32 BOOL (no [MarshalAs] override on the managed -// side), so it's `int*` here; -// * every `uint[] IDs` is a caller-allocated array, so it's `uint*` + count here. -public static unsafe class SchemaApi -{ - private static uint OK => (uint)HRESULT.S_OK.hr; - private static uint Fail => unchecked((uint)HRESULT.E_FAIL.hr); - private static uint InvalidArg => unchecked((uint)HRESULT.E_INVALIDARG.hr); - - // ---- schema-level ---------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpSetSchemaID")] - public static uint SpSetSchemaID(IntPtr schema, ushort id) - { - if (!HandleTable.TryGet(schema, out SchemaRegistration registration)) - return InvalidArg; - registration.ID = id; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryTypeCount")] - public static uint SpQueryTypeCount(IntPtr schema, uint* typeCount) - { - if (typeCount == null || !HandleTable.TryGet(schema, out SchemaRegistration registration)) - return InvalidArg; - *typeCount = (uint)registration.Types.Count; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEnumCount")] - public static uint SpQueryEnumCount(IntPtr schema, uint* enumCount) - { - if (enumCount == null || !HandleTable.TryGet(schema, out SchemaRegistration registration)) - return InvalidArg; - *enumCount = (uint)registration.Enums.Count; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetTypeSchema")] - public static uint SpGetTypeSchema(IntPtr schema, uint index, IntPtr* type, uint* id) - { - if (type == null || id == null || !HandleTable.TryGet(schema, out SchemaRegistration registration)) - return InvalidArg; - if (index >= registration.Types.Count) - return InvalidArg; - - TypeSchema typeSchema = registration.Types[(int)index]; - *type = HandleTable.Alloc(typeSchema); - *id = typeSchema.ID; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetEnumSchema")] - public static uint SpGetEnumSchema(IntPtr schema, uint index, IntPtr* enumType, uint* id) - { - if (enumType == null || id == null || !HandleTable.TryGet(schema, out SchemaRegistration registration)) - return InvalidArg; - if (index >= registration.Enums.Count) - return InvalidArg; - - EnumSchema enumSchema = registration.Enums[(int)index]; - *enumType = HandleTable.Alloc(enumSchema); - *id = enumSchema.ID; - return OK; - } - - // ---- type-level ------------------------------------------------------------------ - - [UnmanagedCallersOnly(EntryPoint = "SpQueryTypeName")] - public static uint SpQueryTypeName(IntPtr typeSchema, char** name) - { - if (name == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *name = NativeString.InternUni(schema.Name); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpIsRuntimeImmutable")] - public static uint SpIsRuntimeImmutable(IntPtr typeSchema, int* isRuntimeImmutable) - { - if (isRuntimeImmutable == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *isRuntimeImmutable = schema.IsRuntimeImmutable ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryBaseType")] - public static uint SpQueryBaseType(IntPtr typeSchema, uint* baseTypeID) - { - if (baseTypeID == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *baseTypeID = SchemaRegistry.GetTypeId(schema.Type.BaseType); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetMarshalAs")] - public static uint SpGetMarshalAs(IntPtr typeSchema, uint* interopEquivalent) - { - if (interopEquivalent == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *interopEquivalent = SchemaRegistry.GetMarshalAs(schema.Type); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryConstructorCount")] - public static uint SpQueryConstructorCount(IntPtr typeSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *count = (uint)schema.Constructors.Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetConstructorSchema")] - public static uint SpGetConstructorSchema(IntPtr typeSchema, uint index, IntPtr* constructor, uint* id) - { - if (constructor == null || id == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (index >= schema.Constructors.Length) - return InvalidArg; - - *constructor = HandleTable.Alloc(schema.Constructors[index]); - *id = index; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyCount")] - public static uint SpQueryPropertyCount(IntPtr typeSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *count = (uint)schema.Properties.Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetPropertySchema")] - public static uint SpGetPropertySchema(IntPtr typeSchema, uint index, IntPtr* property, uint* id) - { - if (property == null || id == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (index >= schema.Properties.Length) - return InvalidArg; - - *property = HandleTable.Alloc(schema.Properties[index]); - *id = index; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryMethodCount")] - public static uint SpQueryMethodCount(IntPtr typeSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *count = (uint)schema.Methods.Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetMethodSchema")] - public static uint SpGetMethodSchema(IntPtr typeSchema, uint index, IntPtr* method, uint* id) - { - if (method == null || id == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (index >= schema.Methods.Length) - return InvalidArg; - - *method = HandleTable.Alloc(schema.Methods[index]); - *id = index; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEventCount")] - public static uint SpQueryEventCount(IntPtr typeSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - *count = (uint)schema.Events.Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetEventSchema")] - public static uint SpGetEventSchema(IntPtr typeSchema, uint index, IntPtr* eventObj, uint* id) - { - if (eventObj == null || id == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (index >= schema.Events.Length) - return InvalidArg; - - *eventObj = HandleTable.Alloc(schema.Events[index]); - *id = index; - return OK; - } - - // ---- construction / invocation --------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpInvokeConstructor")] - public static uint SpInvokeConstructor(IntPtr typeSchema, uint constructorID, UIXVariant* parameters, uint parameterCount, IntPtr* nativeObject) - { - if (nativeObject == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (constructorID >= schema.Constructors.Length) - return InvalidArg; - - try - { - object instance = schema.Constructors[constructorID].Invoke(ToObjects(parameters, parameterCount)); - *nativeObject = HandleTable.Alloc(new SchemaObject(instance, schema)); - return OK; - } - catch (TargetInvocationException) - { - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetPropertyValue")] - public static uint SpGetPropertyValue(IntPtr typeSchema, IntPtr nativeObject, uint propertyID, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (propertyID >= schema.Properties.Length) - return InvalidArg; - - object instance = HandleTable.Get(nativeObject)?.Instance; - try - { - *propertyValue = UIXVariant.FromObject(schema.Properties[propertyID].GetValue(instance)); - return OK; - } - catch (Exception e) when (e is TargetInvocationException or NotSupportedException) - { - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpSetPropertyValue")] - public static uint SpSetPropertyValue(IntPtr typeSchema, IntPtr nativeObject, uint propertyID, UIXVariant* propertyValue) - { - if (propertyValue == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (propertyID >= schema.Properties.Length) - return InvalidArg; - - object instance = HandleTable.Get(nativeObject)?.Instance; - PropertyInfo property = schema.Properties[propertyID]; - try - { - property.SetValue(instance, Coerce(propertyValue->ToObject(), property.PropertyType)); - return OK; - } - catch (Exception e) when (e is TargetInvocationException or NotSupportedException or ArgumentException) - { - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpInvokeMethod")] - public static uint SpInvokeMethod(IntPtr typeSchema, IntPtr nativeObject, uint methodID, UIXVariant* parameters, uint parameterCount, UIXVariant* returnValue) - { - if (returnValue == null || !HandleTable.TryGet(typeSchema, out TypeSchema schema)) - return InvalidArg; - if (methodID >= schema.Methods.Length) - return InvalidArg; - - object instance = HandleTable.Get(nativeObject)?.Instance; - MethodInfo method = schema.Methods[methodID]; - try - { - object[] args = ToObjects(parameters, parameterCount); - ParameterInfo[] expected = method.GetParameters(); - for (int i = 0; i < args.Length && i < expected.Length; i++) - args[i] = Coerce(args[i], expected[i].ParameterType); - - object result = method.Invoke(instance, args); - *returnValue = method.ReturnType == typeof(void) ? UIXVariant.Empty : UIXVariant.FromObject(result); - return OK; - } - catch (Exception e) when (e is TargetInvocationException or NotSupportedException or ArgumentException) - { - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpInvokeToString")] - public static uint SpInvokeToString(IntPtr typeSchema, IntPtr nativeObject, IntPtr* value) - { - if (value == null) - return InvalidArg; - - object instance = HandleTable.Get(nativeObject)?.Instance; - *value = (IntPtr)NativeString.AllocUni(instance?.ToString() ?? string.Empty); - return OK; - } - - // ---- constructor-schema-level ---------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpQueryConstructorParameterCount")] - public static uint SpQueryConstructorParameterCount(IntPtr constructorSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(constructorSchema, out ConstructorInfo constructor)) - return InvalidArg; - *count = (uint)constructor.GetParameters().Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetConstructorParameterTypes")] - public static uint SpGetConstructorParameterTypes(IntPtr constructorSchema, uint* ids, uint count) - { - if (ids == null || !HandleTable.TryGet(constructorSchema, out ConstructorInfo constructor)) - return InvalidArg; - return WriteParameterTypes(constructor.GetParameters(), ids, count); - } - - // ---- property-schema-level ------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyName")] - public static uint SpQueryPropertyName(IntPtr propertySchema, char** name) - { - if (name == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - *name = NativeString.InternUni(property.Name); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyType")] - public static uint SpQueryPropertyType(IntPtr propertySchema, uint* type) - { - if (type == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - *type = SchemaRegistry.GetTypeId(property.PropertyType); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyCanRead")] - public static uint SpQueryPropertyCanRead(IntPtr propertySchema, int* canRead) - { - if (canRead == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - *canRead = property.CanRead ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyCanWrite")] - public static uint SpQueryPropertyCanWrite(IntPtr propertySchema, int* canWrite) - { - if (canWrite == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - *canWrite = property.CanWrite ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyIsStatic")] - public static uint SpQueryPropertyIsStatic(IntPtr propertySchema, int* isStatic) - { - if (isStatic == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - *isStatic = (property.GetMethod ?? property.SetMethod)?.IsStatic == true ? 1 : 0; - return OK; - } - - // A property "notifies on change" if its declaring type raises a change event for it. - // The CLR convention for exactly that is INotifyPropertyChanged, or a per-property - // "Changed" event -- both checked here rather than assuming one or the other. - [UnmanagedCallersOnly(EntryPoint = "SpQueryPropertyNotifiesOnChange")] - [UnconditionalSuppressMessage("Trimming", "IL2075", Justification = "Declaring types originate from runtime-loaded assemblies (SpLoadDll); see SchemaRegistration.FromAssembly.")] - public static uint SpQueryPropertyNotifiesOnChange(IntPtr propertySchema, int* notifiesOnChange) - { - if (notifiesOnChange == null || !HandleTable.TryGet(propertySchema, out PropertyInfo property)) - return InvalidArg; - - Type declaring = property.DeclaringType; - bool notifies = - typeof(System.ComponentModel.INotifyPropertyChanged).IsAssignableFrom(declaring) || - declaring?.GetEvent(property.Name + "Changed") != null; - - *notifiesOnChange = notifies ? 1 : 0; - return OK; - } - - // ---- method-schema-level --------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpQueryMethodName")] - public static uint SpQueryMethodName(IntPtr methodSchema, char** name) - { - if (name == null || !HandleTable.TryGet(methodSchema, out MethodInfo method)) - return InvalidArg; - *name = NativeString.InternUni(method.Name); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryMethodReturnType")] - public static uint SpQueryMethodReturnType(IntPtr methodSchema, uint* type) - { - if (type == null || !HandleTable.TryGet(methodSchema, out MethodInfo method)) - return InvalidArg; - *type = SchemaRegistry.GetTypeId(method.ReturnType); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryMethodParameterCount")] - public static uint SpQueryMethodParameterCount(IntPtr methodSchema, uint* count) - { - if (count == null || !HandleTable.TryGet(methodSchema, out MethodInfo method)) - return InvalidArg; - *count = (uint)method.GetParameters().Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetMethodParameterTypes")] - public static uint SpGetMethodParameterTypes(IntPtr methodSchema, uint* ids, uint count) - { - if (ids == null || !HandleTable.TryGet(methodSchema, out MethodInfo method)) - return InvalidArg; - return WriteParameterTypes(method.GetParameters(), ids, count); - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryMethodIsStatic")] - public static uint SpQueryMethodIsStatic(IntPtr methodSchema, int* isStatic) - { - if (isStatic == null || !HandleTable.TryGet(methodSchema, out MethodInfo method)) - return InvalidArg; - *isStatic = method.IsStatic ? 1 : 0; - return OK; - } - - // ---- event-schema-level ---------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEventName")] - public static uint SpQueryEventName(IntPtr eventSchema, char** name) - { - if (name == null || !HandleTable.TryGet(eventSchema, out EventInfo eventInfo)) - return InvalidArg; - *name = NativeString.InternUni(eventInfo.Name); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEventIsStatic")] - public static uint SpQueryEventIsStatic(IntPtr eventSchema, int* isStatic) - { - if (isStatic == null || !HandleTable.TryGet(eventSchema, out EventInfo eventInfo)) - return InvalidArg; - *isStatic = (eventInfo.AddMethod ?? eventInfo.RemoveMethod)?.IsStatic == true ? 1 : 0; - return OK; - } - - // ---- object lifetime / identity -------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpAddRefExternalObject")] - public static void SpAddRefExternalObject(IntPtr nativeObject) - { - if (HandleTable.TryGet(nativeObject, out SchemaObject obj)) - obj.RefCount++; - } - - [UnmanagedCallersOnly(EntryPoint = "SpReleaseExternalObject")] - public static void SpReleaseExternalObject(IntPtr nativeObject) - { - if (!HandleTable.TryGet(nativeObject, out SchemaObject obj)) - return; - - if (--obj.RefCount <= 0) - HandleTable.Free(nativeObject); - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetTypeID")] - public static uint SpGetTypeID(IntPtr nativeObject, uint* typeID) - { - if (typeID == null || !HandleTable.TryGet(nativeObject, out SchemaObject obj)) - return InvalidArg; - *typeID = SchemaRegistry.GetTypeId(obj.Schema.Type); - return OK; - } - - // "Query for the interface this type marshals as" -- the CLR analogue of QueryInterface - // is a cast, so this succeeds when the object really does implement the requested - // registered type and hands back the same object handle (identity is preserved by a - // reference cast, exactly as QueryInterface on a C++ object would). - [UnmanagedCallersOnly(EntryPoint = "SpQueryForMarshalAsInterface")] - public static uint SpQueryForMarshalAsInterface(IntPtr nativeObject, uint interfaceID, IntPtr* interfaceImpl) - { - if (interfaceImpl == null || !HandleTable.TryGet(nativeObject, out SchemaObject obj)) - return InvalidArg; - - Type requested = SchemaRegistry.GetType(interfaceID); - if (requested == null || !requested.IsInstanceOfType(obj.Instance)) - { - *interfaceImpl = IntPtr.Zero; - return Fail; - } - - obj.RefCount++; - *interfaceImpl = nativeObject; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpSetStateCache")] - public static void SpSetStateCache(IntPtr nativeObject, ulong state) - { - if (HandleTable.TryGet(nativeObject, out SchemaObject obj)) - obj.StateCache = state; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetStateCache")] - public static void SpGetStateCache(IntPtr nativeObject, ulong* state) - { - if (state == null) - return; - *state = HandleTable.TryGet(nativeObject, out SchemaObject obj) ? obj.StateCache : 0UL; - } - - // ---- enum-schema-level ----------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEnumName")] - public static uint SpQueryEnumName(IntPtr nativeObject, char** name) - { - if (name == null || !HandleTable.TryGet(nativeObject, out EnumSchema schema)) - return InvalidArg; - *name = NativeString.InternUni(schema.Name); - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEnumIsFlags")] - public static uint SpQueryEnumIsFlags(IntPtr nativeObject, int* isFlags) - { - if (isFlags == null || !HandleTable.TryGet(nativeObject, out EnumSchema schema)) - return InvalidArg; - *isFlags = schema.IsFlags ? 1 : 0; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpQueryEnumValueCount")] - public static uint SpQueryEnumValueCount(IntPtr nativeObject, uint* valueCount) - { - if (valueCount == null || !HandleTable.TryGet(nativeObject, out EnumSchema schema)) - return InvalidArg; - *valueCount = (uint)schema.Names.Length; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpGetEnumNameValue")] - public static uint SpGetEnumNameValue(IntPtr nativeObject, uint index, char** name, int* value) - { - if (name == null || value == null || !HandleTable.TryGet(nativeObject, out EnumSchema schema)) - return InvalidArg; - if (index >= schema.Names.Length) - return InvalidArg; - - *name = NativeString.InternUni(schema.Names[index]); - *value = schema.Values[index]; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpInvokeEnumToString")] - public static uint SpInvokeEnumToString(IntPtr nativeObject, int value, IntPtr* result) - { - if (result == null || !HandleTable.TryGet(nativeObject, out EnumSchema schema)) - return InvalidArg; - - string text = Enum.ToObject(schema.Type, value).ToString(); - *result = (IntPtr)NativeString.AllocUni(text); - return OK; - } - - // ---- helpers --------------------------------------------------------------------- - - private static object[] ToObjects(UIXVariant* parameters, uint count) - { - if (parameters == null || count == 0) - return Array.Empty(); - - var values = new object[count]; - for (uint i = 0; i < count; i++) - values[i] = parameters[i].ToObject(); - return values; - } - - // Reflection needs the exact declared type (an int variant assigned to a `long` - // parameter would otherwise throw), and UIXVariant only carries a handful of - // primitive shapes -- so widen/narrow to the target here rather than at every call site. - private static object Coerce(object value, Type target) - { - if (value == null || target.IsInstanceOfType(value)) - return value; - if (target.IsEnum) - return Enum.ToObject(target, value); - return Convert.ChangeType(value, target); - } - - private static uint WriteParameterTypes(ParameterInfo[] parameters, uint* ids, uint count) - { - uint writable = Math.Min(count, (uint)parameters.Length); - for (uint i = 0; i < writable; i++) - ids[i] = SchemaRegistry.GetTypeId(parameters[i].ParameterType); - return OK; - } -} - -#endif diff --git a/UIXrender/Subsystems/Schema/SchemaModel.cs b/UIXrender/Subsystems/Schema/SchemaModel.cs deleted file mode 100644 index fec2744..0000000 --- a/UIXrender/Subsystems/Schema/SchemaModel.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; -using System.Linq; -using System.Reflection; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Schema; - -// The managed model behind the ~50 Sp{Query,Get,Invoke}* "native reflection" exports in -// UIX/Microsoft/Iris/OS/NativeApi.cs. -// -// Design decision (deliberate substitution, not a stub -- see logs/UIXrender/FullSurface.md): -// the original exports projected a *C++* type system (the native gadget classes compiled -// into UIXrender.dll) so that markup could bind to them by name. Those C++ classes do not -// exist in this reimplementation and never will. What this layer does instead is project -// ordinary **CLR** types through the exact same export surface, using System.Reflection. -// Every accessor keeps its original signature and semantics ("give me the Nth property of -// this type schema, and its ID"), so the managed caller cannot tell the difference -- but -// the thing on the other side is a registered .NET type rather than a C++ one. -// -// IDs are stable per-schema indices, not pointers: the managed side round-trips them back -// to us (SpGetPropertyValue(typeSchema, obj, propertyID, ...)), so they only need to be -// unique and stable within one schema, which an array index is. - -internal sealed class TypeSchema -{ - // The trimmer/AOT compiler can't see which members are needed, because the types come - // from assemblies loaded at runtime via SpLoadDll -- that is the entire point of this - // subsystem. Annotating the parameter keeps every member of anything that reaches - // here rooted, instead of silently trimming the properties/methods markup binds to. - public TypeSchema( -#if NETCOREAPP - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type type, uint id) - { - Type = type; - ID = id; - - // BindingFlags deliberately include static + instance public members only: - // the original surface exposes IsStatic as a queryable trait of properties, - // methods and events, which only makes sense if both kinds are enumerated. - const BindingFlags flags = BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static; - - Properties = type.GetProperties(flags); - Methods = type.GetMethods(flags).Where(m => !m.IsSpecialName).ToArray(); - Events = type.GetEvents(flags); - Constructors = type.GetConstructors(BindingFlags.Public | BindingFlags.Instance); - } - - public Type Type { get; } - public uint ID { get; } - public PropertyInfo[] Properties { get; } - public MethodInfo[] Methods { get; } - public EventInfo[] Events { get; } - public ConstructorInfo[] Constructors { get; } - - public string Name => Type.Name; - - // "Runtime immutable" in the original means a value that can never change once - // constructed -- the closest faithful CLR reading is a type with no writable - // instance state at all. - public bool IsRuntimeImmutable => - Type.IsPrimitive || Type == typeof(string) || Properties.All(p => !p.CanWrite); -} - -internal sealed class EnumSchema -{ - public EnumSchema(Type type, uint id) - { - Type = type; - ID = id; - Names = Enum.GetNames(type); - - // GetValuesAsUnderlyingType rather than GetValues(Type): the latter is - // RequiresDynamicCode (IL3050) because it has to construct a T[] of the enum type - // at runtime, which can genuinely fail under NativeAOT -- and this project - // publishes AOT. The underlying-type overload returns a boxed primitive array, no - // dynamic array construction involved. - Array underlying = -#if NETCOREAPP - Enum.GetValuesAsUnderlyingType(type); -#else - Enum.GetValues(type); -#endif - - Values = new int[underlying.Length]; - for (int i = 0; i < underlying.Length; i++) - Values[i] = Convert.ToInt32(underlying.GetValue(i)); - } - - public Type Type { get; } - public uint ID { get; } - public string[] Names { get; } - public int[] Values { get; } - - public string Name => Type.Name; - public bool IsFlags => Type.IsDefined(typeof(FlagsAttribute), false); -} - -// One registered schema (the unit SpSetSchemaID/SpQueryTypeCount/SpGetTypeSchema operate -// on) -- a set of types and enums that were loaded together, e.g. from one markup-visible -// assembly loaded via SpLoadDll. -internal sealed class SchemaRegistration -{ - private readonly List _types = new(); - private readonly List _enums = new(); - - public ushort ID { get; set; } - - public IReadOnlyList Types => _types; - public IReadOnlyList Enums => _enums; - - public void Add( -#if NETCOREAPP - [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] -#endif - Type type) - { - if (type.IsEnum) - _enums.Add(new EnumSchema(type, (uint)_enums.Count)); - else - _types.Add(new TypeSchema(type, (uint)_types.Count)); - } - -#if NETCOREAPP - // Unavoidably trim-unsafe by design, and suppressed rather than left to warn so a real - // future warning isn't lost in the noise: this projects types out of an assembly the - // host chose at *runtime* (SpLoadDll), which the trimmer cannot see into by - // definition. Anything markup binds to must therefore be kept alive by the host's own - // trimming configuration (e.g. a TrimmerRootAssembly entry for each markup-visible - // assembly), not by static analysis of UIXrender. - [UnconditionalSuppressMessage("Trimming", "IL2026", Justification = "Types come from assemblies loaded at runtime via SpLoadDll; the host must root them. See comment above.")] - [UnconditionalSuppressMessage("Trimming", "IL2072", Justification = "Types come from assemblies loaded at runtime via SpLoadDll; the host must root them. See comment above.")] -#endif - public static SchemaRegistration FromAssembly(Assembly assembly) - { - var registration = new SchemaRegistration(); - foreach (Type type in assembly.GetExportedTypes()) - registration.Add(type); - return registration; - } -} - -// A live instance handed back through SpInvokeConstructor and passed into -// SpGetPropertyValue/SpInvokeMethod. Refcounted because the original surface exposes -// SpAddRefExternalObject/SpReleaseExternalObject as an explicit COM-style pair. -internal sealed class SchemaObject -{ - public SchemaObject(object instance, TypeSchema schema) - { - Instance = instance; - Schema = schema; - RefCount = 1; - } - - public object Instance { get; } - public TypeSchema Schema { get; } - public int RefCount { get; set; } - - - // Backs SpGetStateCache/SpSetStateCache -- an opaque 64-bit slot the caller uses to - // memoise its own per-object state; UIXrender only stores and returns it. - public ulong StateCache { get; set; } - - // Backs SpDataBaseObjectGet/SetInternalHandle -- the framework-side handle for this - // object, likewise opaque to us. - public ulong InternalHandle { get; set; } -} diff --git a/UIXrender/Subsystems/Schema/SchemaRegistry.cs b/UIXrender/Subsystems/Schema/SchemaRegistry.cs deleted file mode 100644 index 8791bc3..0000000 --- a/UIXrender/Subsystems/Schema/SchemaRegistry.cs +++ /dev/null @@ -1,75 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using Microsoft.Iris.Render.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Schema; - -// Process-wide type-ID assignment for the schema subsystem. Several exports return a -// *type ID* rather than a schema pointer (SpQueryBaseType, SpQueryPropertyType, -// SpQueryMethodReturnType, SpGetMethodParameterTypes, SpGetTypeID), and the managed side -// round-trips those IDs across unrelated calls -- so they have to be stable and unique -// process-wide, not per-schema. An incrementing counter keyed by CLR Type gives exactly -// that. -// -// IDs 0-15 are reserved for the primitives that UIXVariant can carry directly, so a -// caller can recognise "this property is an int" without a schema lookup. That mirrors -// what the original must have done to make UIXVariant's VariantType tag meaningful, and -// is the only part of this mapping that isn't an arbitrary counter. -internal static class SchemaRegistry -{ - public const uint TypeIdNone = 0; - - private static readonly Dictionary s_wellKnown = new() - { - [typeof(void)] = 0, - [typeof(bool)] = (uint)VariantType.Bool, - [typeof(byte)] = (uint)VariantType.Byte, - [typeof(int)] = (uint)VariantType.Int32, - [typeof(long)] = (uint)VariantType.Int64, - [typeof(float)] = (uint)VariantType.Single, - [typeof(double)] = (uint)VariantType.Double, - }; - - private const uint FirstDynamicTypeId = 16; - - private static readonly ConcurrentDictionary s_typeIds = new(); - private static readonly ConcurrentDictionary s_typesById = new(); - private static uint s_nextTypeId = FirstDynamicTypeId; - - public static uint GetTypeId(Type type) - { - if (type == null) - return TypeIdNone; - - if (s_wellKnown.TryGetValue(type, out uint wellKnown)) - return wellKnown; - - return s_typeIds.GetOrAdd(type, static t => - { - uint id = System.Threading.Interlocked.Increment(ref s_nextTypeId); - s_typesById[id] = t; - return id; - }); - } - - public static Type GetType(uint typeId) => s_typesById.TryGetValue(typeId, out Type type) ? type : null; - - // Backs SpGetMarshalAs: "what does this type look like to the interop layer". For - // anything UIXVariant can carry natively that's the VariantType tag; for everything - // else it's UIXObject, i.e. "marshal it as an opaque object handle". - public static uint GetMarshalAs(Type type) - { - if (type == null) - return (uint)VariantType.Empty; - if (type == typeof(bool)) return (uint)VariantType.Bool; - if (type == typeof(byte)) return (uint)VariantType.Byte; - if (type == typeof(int)) return (uint)VariantType.Int32; - if (type == typeof(long)) return (uint)VariantType.Int64; - if (type == typeof(float)) return (uint)VariantType.Single; - if (type == typeof(double)) return (uint)VariantType.Double; - if (type == typeof(string)) return (uint)VariantType.UIXString; - if (type.IsEnum) return (uint)VariantType.Enum; - return (uint)VariantType.UIXObject; - } -} diff --git a/UIXrender/Subsystems/Text/FontStore.cs b/UIXrender/Subsystems/Text/FontStore.cs deleted file mode 100644 index 2145102..0000000 --- a/UIXrender/Subsystems/Text/FontStore.cs +++ /dev/null @@ -1,190 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.IO; -using System.Linq; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// Resolves a TextStyle font-face name (e.g. "Segoe UI") to a LoadedFont, in priority -// order: fonts registered at runtime (SpLoadFontResource), then TrueType files discovered -// in the platform's font directories, then a first-available fallback. Results are cached -// by the requested face name so repeated measures don't re-scan or re-load. -// -// Platform font-directory discovery is OS file-path logic, not a graphics API, so it's -// gated the ordinary way (CLAUDE.md's platform rule) and degrades honestly: if nothing -// resolves, Resolve returns null and callers fall back to the ratio-based TextMetrics -// (measurement) or report E_NOTIMPL (rasterization) rather than faking output. -internal static class FontStore -{ - private static readonly object s_lock = new(); - private static readonly ConcurrentDictionary s_byFace = new(StringComparer.OrdinalIgnoreCase); - private static readonly ConcurrentDictionary s_registered = new(StringComparer.OrdinalIgnoreCase); - - // filename-key (normalized, no extension) -> full path; built once, lazily. - private static Dictionary s_systemIndex; - - // Registers font bytes under a family name (backs SpLoadFontResource). Takes priority - // over system fonts for that family. - public static bool Register(string family, byte[] ttf) - { - if (string.IsNullOrEmpty(family)) - return false; - - LoadedFont font = LoadedFont.TryLoad(family, ttf); - if (font == null) - return false; - - s_registered[Normalize(family)] = font; - s_byFace[family] = font; // fast-path this exact name - return true; - } - - // Resolves a face name to a font, or null if none is available. `faceName` may be null - // (the caller has no specific face), in which case the fallback font is used. - public static LoadedFont Resolve(string faceName) - { - string key = faceName ?? ""; - if (s_byFace.TryGetValue(key, out LoadedFont cached)) - return cached; - - lock (s_lock) - { - if (s_byFace.TryGetValue(key, out cached)) - return cached; - - LoadedFont resolved = ResolveUncached(faceName); - if (resolved != null) - s_byFace[key] = resolved; - return resolved; - } - } - - private static LoadedFont ResolveUncached(string faceName) - { - string norm = Normalize(faceName); - - if (!string.IsNullOrEmpty(norm) && s_registered.TryGetValue(norm, out LoadedFont reg)) - return reg; - - Dictionary index = SystemIndex(); - - // Exact normalized match first (e.g. "segoeui"), then a prefix/contains match - // (so "Segoe UI" also finds "segoeui" / "SegoeUI-Regular"). - if (!string.IsNullOrEmpty(norm)) - { - if (index.TryGetValue(norm, out string exact) && TryLoadFile(faceName, exact, out LoadedFont f)) - return f; - - string best = index - .Where(kv => kv.Key.StartsWith(norm, StringComparison.Ordinal) || kv.Key.Contains(norm)) - .OrderBy(kv => kv.Key.Length) // shortest ~= the plain "Regular" variant - .Select(kv => kv.Value) - .FirstOrDefault(); - - if (best != null && TryLoadFile(faceName, best, out LoadedFont matched)) - return matched; - } - - // Fallback: a common sans-serif if present, else any discovered face. - foreach (string preferred in new[] { "dejavusans", "liberationsans", "arial", "segoeui", "verdana", "notosans", "roboto" }) - { - if (index.TryGetValue(preferred, out string path) && TryLoadFile(faceName ?? preferred, path, out LoadedFont pref)) - return pref; - } - - foreach (string any in index.Values) - { - if (TryLoadFile(faceName ?? "default", any, out LoadedFont anyFont)) - return anyFont; - } - - return null; - } - - private static bool TryLoadFile(string faceName, string path, out LoadedFont font) - { - font = null; - try - { - font = LoadedFont.TryLoad(faceName, File.ReadAllBytes(path)); - return font != null; - } - catch (IOException) - { - return false; - } - } - - private static Dictionary SystemIndex() - { - if (s_systemIndex != null) - return s_systemIndex; - - var index = new Dictionary(StringComparer.Ordinal); - foreach (string dir in FontDirectories()) - { - if (!Directory.Exists(dir)) - continue; - - IEnumerable files; - try - { - files = Directory.EnumerateFiles(dir, "*.ttf", SearchOption.AllDirectories); - } - catch (IOException) - { - continue; - } - catch (UnauthorizedAccessException) - { - continue; - } - - foreach (string file in files) - { - string key = Normalize(Path.GetFileNameWithoutExtension(file)); - // First writer wins, so earlier (more preferred) directories take priority. - index.TryAdd(key, file); - } - } - - s_systemIndex = index; - return index; - } - - private static IEnumerable FontDirectories() - { -#if WINDOWS - yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Windows), "Fonts"); - yield return Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "Microsoft", "Windows", "Fonts"); -#else - // Linux + macOS common locations. TODO: consult fontconfig on Linux for a - // face-name->file mapping instead of filename heuristics, once a suitable - // abstraction is available. - string home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); - yield return "/usr/share/fonts"; - yield return "/usr/local/share/fonts"; - yield return Path.Combine(home, ".fonts"); - yield return Path.Combine(home, ".local", "share", "fonts"); - yield return "/System/Library/Fonts"; - yield return "/Library/Fonts"; - yield return Path.Combine(home, "Library", "Fonts"); -#endif - } - - private static string Normalize(string name) - { - if (string.IsNullOrEmpty(name)) - return ""; - - Span buffer = name.Length <= 128 ? stackalloc char[name.Length] : new char[name.Length]; - int n = 0; - foreach (char c in name) - { - if (char.IsLetterOrDigit(c)) - buffer[n++] = char.ToLowerInvariant(c); - } - return new string(buffer[..n]); - } -} diff --git a/UIXrender/Subsystems/Text/GlyphRun.cs b/UIXrender/Subsystems/Text/GlyphRun.cs deleted file mode 100644 index 49ab2e0..0000000 --- a/UIXrender/Subsystems/Text/GlyphRun.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Buffers; -using System.Runtime.InteropServices; -using System.Text; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// A measured, rasterizable line of text -- the object the `hGlyphRunInfo` handle points at -// between SpSimpleTextMeasure (which builds it) and SpRichTextRasterize (which draws it). -// Backend-agnostic: rasterization composites stb_truetype's CPU coverage bitmaps into a -// straight-alpha ARGB32 buffer (0xAARRGGBB in memory == BGRA little-endian, matching the -// SurfaceFormat.ARGB32 convention BitmapStore already uses). -internal sealed class GlyphRun -{ - public GlyphRun(LoadedFont font, string text, float pixelHeight, Color color, Size size) - { - Font = font; - Text = text ?? string.Empty; - PixelHeight = pixelHeight; - Color = color; - Size = size; - } - - public LoadedFont Font { get; } - public string Text { get; } - public float PixelHeight { get; } - public Color Color { get; } - public Size Size { get; } - - // Produces a straight-alpha ARGB32 bitmap of the measured size, filled with `color` - // (SpRichTextRasterize re-supplies the text colour at draw time). Caller owns the - // returned buffer (freed via SpFreeDib). Returns IntPtr.Zero for an empty run or when - // no real font is available (the ratio fallback can measure but cannot rasterize). - public unsafe IntPtr Rasterize(Color color, out Size size) - { - size = Size; - if (Font == null || Size.width <= 0 || Size.height <= 0 || Text.Length == 0) - return IntPtr.Zero; - - int width = Size.width; - int height = Size.height; - int byteCount = width * height * 4; - - IntPtr buffer = Marshal.AllocHGlobal(byteCount); - var dst = (byte*)buffer; - for (int i = 0; i < byteCount; i++) - dst[i] = 0; - - float scale = Font.ScaleForPixelHeight(PixelHeight); - int baseline = (int)MathF.Round(Font.AscentPx(scale)); - byte colorR = color.R, colorG = color.G, colorB = color.B, colorA = color.A; - - float penX = 0f; - int previous = 0; - - foreach (Rune rune in Text.EnumerateRunes()) - { - int cp = rune.Value; - if (previous != 0) - penX += Font.KerningPx(previous, cp, scale); - - Font.GetGlyphBox(cp, scale, out int ix0, out int iy0, out int ix1, out int iy1); - int gw = ix1 - ix0; - int gh = iy1 - iy0; - - if (gw > 0 && gh > 0) - { - byte[] coverage = ArrayPool.Shared.Rent(gw * gh); - try - { - fixed (byte* cov = coverage) - { - Font.RenderGlyphCoverage(cp, scale, cov, gw, gh, gw); - Blit(dst, width, height, cov, gw, gh, (int)MathF.Round(penX) + ix0, baseline + iy0, colorR, colorG, colorB, colorA); - } - } - finally - { - ArrayPool.Shared.Return(coverage); - } - } - - penX += Font.AdvancePx(cp, scale); - previous = cp; - } - - return buffer; - } - - private static unsafe void Blit(byte* dst, int dstW, int dstH, byte* coverage, int gw, int gh, int originX, int originY, byte r, byte g, byte b, byte a) - { - for (int gy = 0; gy < gh; gy++) - { - int dy = originY + gy; - if ((uint)dy >= (uint)dstH) - continue; - - for (int gx = 0; gx < gw; gx++) - { - byte cov = coverage[gy * gw + gx]; - if (cov == 0) - continue; - - int dx = originX + gx; - if ((uint)dx >= (uint)dstW) - continue; - - byte alpha = (byte)(cov * a / 255); - byte* p = dst + (dy * dstW + dx) * 4; - // Keep the strongest coverage where glyphs happen to overlap. - if (alpha >= p[3]) - { - p[0] = b; - p[1] = g; - p[2] = r; - p[3] = alpha; - } - } - } - } -} - -// Owns the unmanaged ARGB buffer handed back as `phTextBitmap`; freed by SpFreeDib. -internal sealed class TextBitmap(IntPtr bits) : IDisposable -{ - private IntPtr _bits = bits; - - public IntPtr Bits => _bits; - - public void Dispose() - { - if (_bits != IntPtr.Zero) - { - Marshal.FreeHGlobal(_bits); - _bits = IntPtr.Zero; - } - } -} diff --git a/UIXrender/Subsystems/Text/LoadedFont.cs b/UIXrender/Subsystems/Text/LoadedFont.cs deleted file mode 100644 index 23f6922..0000000 --- a/UIXrender/Subsystems/Text/LoadedFont.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using StbTrueTypeSharp; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// A TrueType face loaded via StbTrueTypeSharp (pure-managed stb_truetype). Backend- -// agnostic: this touches no GPU/window API -- it only produces glyph metrics and 8-bit -// coverage bitmaps in CPU memory, which is exactly what SpSimpleTextMeasure / -// SpRichTextMeasure / SpRichTextRasterize need. -// -// stb_truetype stores the raw font-data *pointer* inside stbtt_fontinfo (it does not copy -// by default), so the backing byte[] must stay fixed for the font's whole lifetime -- -// hence the pinned GCHandle held here and freed in Dispose. -internal sealed unsafe class LoadedFont : IDisposable -{ - private readonly byte[] _data; - private GCHandle _pin; - private readonly StbTrueType.stbtt_fontinfo _info; - - public string Family { get; } - - // Unscaled (font-design-unit) vertical metrics; multiply by ScaleForPixelHeight(px). - public int AscentUnscaled { get; } - public int DescentUnscaled { get; } - public int LineGapUnscaled { get; } - - private LoadedFont(string family, byte[] data, GCHandle pin, StbTrueType.stbtt_fontinfo info) - { - Family = family; - _data = data; - _pin = pin; - _info = info; - - int ascent, descent, lineGap; - StbTrueType.stbtt_GetFontVMetrics(info, &ascent, &descent, &lineGap); - AscentUnscaled = ascent; - DescentUnscaled = descent; - LineGapUnscaled = lineGap; - } - - public static LoadedFont TryLoad(string family, byte[] data) - { - if (data == null || data.Length == 0) - return null; - - GCHandle pin = GCHandle.Alloc(data, GCHandleType.Pinned); - var info = new StbTrueType.stbtt_fontinfo(); - var p = (byte*)pin.AddrOfPinnedObject(); - - int offset = StbTrueType.stbtt_GetFontOffsetForIndex(p, 0); - if (offset < 0 || StbTrueType.stbtt_InitFont(info, p, offset) == 0) - { - pin.Free(); - return null; - } - - return new LoadedFont(family, data, pin, info); - } - - public float ScaleForPixelHeight(float px) => StbTrueType.stbtt_ScaleForPixelHeight(_info, px); - - public float LineHeightPx(float scale) => (AscentUnscaled - DescentUnscaled + LineGapUnscaled) * scale; - - public float AscentPx(float scale) => AscentUnscaled * scale; - - public float AdvancePx(int codepoint, float scale) - { - int advance, leftSideBearing; - StbTrueType.stbtt_GetCodepointHMetrics(_info, codepoint, &advance, &leftSideBearing); - return advance * scale; - } - - public float KerningPx(int codepoint1, int codepoint2, float scale) => - StbTrueType.stbtt_GetCodepointKernAdvance(_info, codepoint1, codepoint2) * scale; - - public void GetGlyphBox(int codepoint, float scale, out int ix0, out int iy0, out int ix1, out int iy1) - { - int x0, y0, x1, y1; - StbTrueType.stbtt_GetCodepointBitmapBox(_info, codepoint, scale, scale, &x0, &y0, &x1, &y1); - ix0 = x0; iy0 = y0; ix1 = x1; iy1 = y1; - } - - // Renders one glyph's 8-bit coverage into a caller-owned buffer (no allocation, no - // free -- unlike stbtt_GetCodepointBitmap which mallocs). - public void RenderGlyphCoverage(int codepoint, float scale, byte* output, int width, int height, int stride) => - StbTrueType.stbtt_MakeCodepointBitmap(_info, output, width, height, stride, scale, scale, codepoint); - - public void Dispose() - { - if (_pin.IsAllocated) - _pin.Free(); - } -} diff --git a/UIXrender/Subsystems/Text/RichTextApi.cs b/UIXrender/Subsystems/Text/RichTextApi.cs deleted file mode 100644 index eca5a87..0000000 --- a/UIXrender/Subsystems/Text/RichTextApi.cs +++ /dev/null @@ -1,347 +0,0 @@ -#if NETCOREAPP - -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Drawing; -using Microsoft.Iris.Render.Interop.Text; -using Microsoft.Iris.Render.Interop.Win32; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// [UnmanagedCallersOnly] exports for the SpRichText*/SpSimpleText* families in -// UIX/Microsoft/Iris/OS/NativeApi.cs (~35 entry points). -// -// Editing, selection, clipboard, undo/redo and all the mode flags are real (see -// RichTextObject). Measurement is approximate (see TextMetrics) and rasterization is not -// implemented -- both flagged in logs/UIXrender/FullSurface.md rather than quietly -// returning plausible-looking output. -public static unsafe class RichTextApi -{ - // Win32 message ids the managed side forwards; declared in NativeApi.cs as constants. - private const uint WM_KEYDOWN = 0x0100; - private const uint WM_CHAR = 0x0102; - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextBuildObject")] - public static HRESULT SpRichTextBuildObject(int fRichTextMode, Size sizeMaximumSurface, IntPtr pCallbacks, HANDLE* hRto) - { - if (hRto == null) - return HRESULT.E_INVALIDARG; - - Interop.Com.ComVtable.AddRef(pCallbacks); - var text = new RichTextObject(fRichTextMode != 0, sizeMaximumSurface, pCallbacks); - hRto->h = HandleTable.Alloc(text); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextDestroyObject")] - public static void SpRichTextDestroyObject(HANDLE hRto) - { - if (HandleTable.TryGet(hRto.h, out RichTextObject text)) - Interop.Com.ComVtable.Release(text.Callbacks); - HandleTable.Free(hRto.h); - } - - // ---- content ----------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetContent")] - public static HRESULT SpRichTextSetContent(HANDLE hRto, char* pszContent) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - text.SetContent(NativeString.UniToString(pszContent)); - return HRESULT.S_OK; - } - - // Writes into the caller's StringBuilder buffer (marshaled as a char* of cchBuffer - // characters), NUL-terminated and never overrunning. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextGetSimpleContent")] - public static HRESULT SpRichTextGetSimpleContent(HANDLE hRto, char* textBuffer, int cchBuffer) - { - if (textBuffer == null || cchBuffer <= 0 || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - string content = text.Text; - int copy = Math.Min(content.Length, cchBuffer - 1); - content.AsSpan(0, copy).CopyTo(new Span(textBuffer, copy)); - textBuffer[copy] = '\0'; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextGetSimpleContentLength")] - public static HRESULT SpRichTextGetSimpleContentLength(HANDLE hRto, int* textLength) - { - if (textLength == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - *textLength = text.Length; - return HRESULT.S_OK; - } - - // ---- clipboard / editing ----------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextCopy")] - public static HRESULT SpRichTextCopy(HANDLE hRto) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - text.Copy(); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextCut")] - public static HRESULT SpRichTextCut(HANDLE hRto) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - return text.Cut() ? HRESULT.S_OK : HRESULT.E_FAIL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextPaste")] - public static HRESULT SpRichTextPaste(HANDLE hRto) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - return text.Paste() ? HRESULT.S_OK : HRESULT.E_FAIL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextDelete")] - public static HRESULT SpRichTextDelete(HANDLE hRto) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - return text.DeleteSelection() ? HRESULT.S_OK : HRESULT.E_FAIL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextCanUndo")] - public static HRESULT SpRichTextCanUndo(HANDLE hRto, int* canUndo) - { - if (canUndo == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - *canUndo = text.CanUndo ? 1 : 0; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextUndo")] - public static HRESULT SpRichTextUndo(HANDLE hRto) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - return text.Undo() ? HRESULT.S_OK : HRESULT.E_FAIL; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetSelectionRange")] - public static HRESULT SpRichTextSetSelectionRange(HANDLE hRto, int selectionStart, int selectionEnd) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - text.SetSelectionRange(selectionStart, selectionEnd); - return HRESULT.S_OK; - } - - // ---- mode flags -------------------------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetReadOnly")] - public static HRESULT SpRichTextSetReadOnly(HANDLE hRto, int readOnly) => - Apply(hRto, t => t.ReadOnly = readOnly != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetWordWrap")] - public static HRESULT SpRichTextSetWordWrap(HANDLE hRto, int fWordWrap) => - Apply(hRto, t => t.WordWrap = fWordWrap != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetMaximumLength")] - public static HRESULT SpRichTextSetMaximumLength(HANDLE hRto, int maximumLength) => - Apply(hRto, t => t.MaximumLength = maximumLength > 0 ? maximumLength : int.MaxValue); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetDetectUrls")] - public static HRESULT SpRichTextSetDetectUrls(HANDLE hRto, int detectUrls) => - Apply(hRto, t => t.DetectUrls = detectUrls != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetOversampleMode")] - public static HRESULT SpRichTextSetOversampleMode(HANDLE hRto, int fOversample) => - Apply(hRto, t => t.Oversample = fOversample != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetScale")] - public static HRESULT SpRichTextSetScale(HANDLE hRto, float flScale) => - Apply(hRto, t => t.Scale = flScale); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetScrollbars")] - public static HRESULT SpRichTextSetScrollbars(HANDLE hRto, int allowVertical, int allowHorizontal) => - Apply(hRto, t => - { - t.AllowVerticalScroll = allowVertical != 0; - t.AllowHorizontalScroll = allowHorizontal != 0; - }); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextSetFocus")] - public static HRESULT SpRichTextSetFocus(HANDLE hRto, int gainingFocus) => - Apply(hRto, t => t.SetFocus(gainingFocus != 0)); - - // ---- input forwarding -------------------------------------------------------- - - // Real: a WM_CHAR carrying a printable character is inserted into the buffer (which - // fires TextChanged and honours read-only/max-length), anything else is reported - // unhandled so the managed side can act on it. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextForwardKeyCharacter")] - public static HRESULT SpRichTextForwardKeyCharacter(HANDLE hRto, uint message, int character, int scanCode, int repeatCount, uint modifierState, ushort flags, int* handled) - { - if (handled == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - *handled = 0; - if (message != WM_CHAR || character < ' ') - return HRESULT.S_OK; - - var inserted = new string((char)character, Math.Max(1, repeatCount)); - *handled = text.InsertText(inserted) ? 1 : 0; - return HRESULT.S_OK; - } - - // Editing keys (backspace/delete) act on the buffer; navigation and everything else - // is left to the managed side, which owns caret movement. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextForwardKeyState")] - public static HRESULT SpRichTextForwardKeyState(HANDLE hRto, uint message, int virtualKey, int scanCode, int repeatCount, uint modifierState, ushort flags, int* handled) - { - const int VK_BACK = 0x08; - const int VK_DELETE = 0x2E; - - if (handled == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - *handled = 0; - if (message != WM_KEYDOWN) - return HRESULT.S_OK; - - if (virtualKey is VK_BACK or VK_DELETE) - *handled = text.DeleteSelection() ? 1 : 0; - - return HRESULT.S_OK; - } - - // Mouse hit-testing needs real glyph positions to map a point to a character offset, - // which this implementation doesn't have (see TextMetrics). Reports unhandled rather - // than moving the caret to a wrong offset. - // TODO: implement alongside a real font backend. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextForwardMouseInput")] - public static HRESULT SpRichTextForwardMouseInput(HANDLE hRto, uint message, uint modifierState, int mouseButton, int x, int y, int mouseWheelDelta, int* handled) - { - if (handled == null) - return HRESULT.E_INVALIDARG; - *handled = 0; - return HRESULT.S_OK; - } - - // IME composition needs a platform input-method context to interpret; the managed - // side's own IME plumbing (SpRegisterImeCallbacks) is the path that actually carries - // these today. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextForwardImeMessage")] - public static HRESULT SpRichTextForwardImeMessage(HANDLE hRto, uint message, UIntPtr wParam, UIntPtr lParam) => HRESULT.S_OK; - - // ---- scrolling / timers ------------------------------------------------------ - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextScroll")] - public static HRESULT SpRichTextScroll(HANDLE hRto, int whichBar, int scrollType) => - Apply(hRto, static _ => { }); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextScrollToPosition")] - public static HRESULT SpRichTextScrollToPosition(HANDLE hRto, int whichBar, int whereTo) => - Apply(hRto, static _ => { }); - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextOnTimerTick")] - public static HRESULT SpRichTextOnTimerTick(HANDLE hRto, uint timerId) => - Apply(hRto, static t => t.NotifyShowCaret(t.HasFocus)); - - // ---- measurement / rasterization --------------------------------------------- - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextGetNaturalBounds")] - public static HRESULT SpRichTextGetNaturalBounds(HANDLE hRto, int* cWidth, int* cHeight) - { - if (cWidth == null || cHeight == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - // Font height isn't carried on the object (it arrives per-measure in a TextStyle), - // so natural bounds use the surface's own height as the em size reference. No face - // is known here, so the fallback font resolves. - LoadedFont font = FontStore.Resolve(null); - Size bounds = TextLayout.Measure(font, text.Text, DefaultFontHeight, text.WordWrap, text.MaximumSurface.width); - *cWidth = bounds.width; - *cHeight = bounds.height; - return HRESULT.S_OK; - } - - private const float DefaultFontHeight = 12f; - - // Real measurement over the resolved font (or the ratio fallback). The per-run - // ReportRunCallback is still not invoked -- rich text's multi-run layout isn't modelled - // yet; a caller gets correct overall bounds via the returned constraint size. Rich-text - // rasterization runs through GlyphRun handles produced by the simple-text path. - // TODO: emit real per-run glyph runs through rrcb. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextMeasure")] - public static HRESULT SpRichTextMeasure(HANDLE hRto, TextMeasureParamsData* measureParams, IntPtr rrcb, IntPtr pvData) - { - if (measureParams == null || !HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - float fontHeight = measureParams->pTextStyle != null && measureParams->pTextStyle->fontHeightPts > 0 - ? measureParams->pTextStyle->fontHeightPts - : DefaultFontHeight; - - string face = measureParams->pTextStyle != null ? NativeString.UniToString(measureParams->pTextStyle->fontFace) : null; - string content = measureParams->content != null ? NativeString.UniToString(measureParams->content) : text.Text; - bool wordWrap = (measureParams->flags & TextMeasureFlags.WordWrapValue) != 0; - int constraint = (int)measureParams->constraint.width; - - LoadedFont font = FontStore.Resolve(face); - Size measured = TextLayout.Measure(font, content, fontHeight, wordWrap, constraint); - measureParams->constraint = new SizeF { width = measured.width, height = measured.height }; - return HRESULT.S_OK; - } - - // Composites the glyph run into a straight-alpha ARGB32 bitmap. `phTextBitmap` is a - // handle SpFreeDib frees; `ppvBits` points at the pixels. Returns S_FALSE-shaped - // failure (E_FAIL) only if the run can't be resolved; an empty/fallback run yields a - // null bitmap with S_OK (nothing to draw), not a fake. - // Outline/shadow modes are accepted but not yet rendered. - // TODO: outline + shadow passes. - [UnmanagedCallersOnly(EntryPoint = "SpRichTextRasterize")] - public static HRESULT SpRichTextRasterize(IntPtr hGlyphRunInfo, int fOutlineMode, Color clrText, int fShadowMode, IntPtr* phTextBitmap, IntPtr* ppvBits, Size* psizeBitmap) - { - if (phTextBitmap != null) *phTextBitmap = IntPtr.Zero; - if (ppvBits != null) *ppvBits = IntPtr.Zero; - if (psizeBitmap != null) *psizeBitmap = default; - - if (!HandleTable.TryGet(hGlyphRunInfo, out GlyphRun run)) - return HRESULT.E_INVALIDARG; - - IntPtr bits = run.Rasterize(clrText, out Size size); - if (psizeBitmap != null) *psizeBitmap = size; - - if (bits == IntPtr.Zero) - return HRESULT.S_OK; // nothing to draw (empty run or no real font) - - if (phTextBitmap != null) *phTextBitmap = HandleTable.Alloc(new TextBitmap(bits)); - if (ppvBits != null) *ppvBits = bits; - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpRichTextDestroyGlyphRunInfo")] - public static void SpRichTextDestroyGlyphRunInfo(IntPtr hGlyphRunInfo) => HandleTable.Free(hGlyphRunInfo); - - // ---- helpers ----------------------------------------------------------------- - - private static HRESULT Apply(HANDLE hRto, Action action) - { - if (!HandleTable.TryGet(hRto.h, out RichTextObject text)) - return HRESULT.E_INVALIDARG; - - action(text); - return HRESULT.S_OK; - } -} - -#endif diff --git a/UIXrender/Subsystems/Text/RichTextObject.cs b/UIXrender/Subsystems/Text/RichTextObject.cs deleted file mode 100644 index 214d96b..0000000 --- a/UIXrender/Subsystems/Text/RichTextObject.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Iris.Render.Interop.Com; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// The managed model behind the SpRichText* family (UIX/Microsoft/Iris/OS/NativeApi.cs) -- -// the native text-box engine backing Iris's editable TextBox. -// -// The *editing* half of this is fully real: content, selection, clipboard operations, -// undo/redo, word wrap, read-only, max length, scale, scrollbars and timers all behave -// the way an edit control does, and the registered IRichTextCallbacks is notified on the -// same occasions the original would have notified it (TextChanged, SelectionChanged, -// MaxLengthExceeded, ...). That matters because the managed TextBox drives its whole -// state machine off those callbacks. -// -// The *typography* half (measuring and rasterizing glyphs) is where this stops short -- -// see RichTextApi.SpRichTextMeasure/SpRichTextRasterize and the open question in -// logs/UIXrender/FullSurface.md. -internal sealed class RichTextObject -{ - // Slot layout of IRichTextCallbacks (UIX/Microsoft/Iris/OS/IRichTextCallbacks.cs), - // declaration order after the three IUnknown slots. Read off that file, not guessed. - private const int SlotInvalidateContent = ComVtable.FirstMethodSlot + 0; - private const int SlotSelectionChanged = ComVtable.FirstMethodSlot + 1; - private const int SlotCreateCaret = ComVtable.FirstMethodSlot + 2; - private const int SlotSetCaretPos = ComVtable.FirstMethodSlot + 3; - private const int SlotShowCaret = ComVtable.FirstMethodSlot + 4; - private const int SlotSetCursor = ComVtable.FirstMethodSlot + 5; - private const int SlotTextChanged = ComVtable.FirstMethodSlot + 6; - private const int SlotMaxLengthExceeded = ComVtable.FirstMethodSlot + 7; - private const int SlotSetTimer = ComVtable.FirstMethodSlot + 8; - private const int SlotKillTimer = ComVtable.FirstMethodSlot + 9; - private const int SlotSetScrollRange = ComVtable.FirstMethodSlot + 10; - private const int SlotEnableScrollbar = ComVtable.FirstMethodSlot + 11; - private const int SlotClientToWindow = ComVtable.FirstMethodSlot + 12; - private const int SlotClientToScreen = ComVtable.FirstMethodSlot + 13; - private const int SlotLinkClicked = ComVtable.FirstMethodSlot + 14; - - private readonly StringBuilder _content = new(); - private readonly Stack _undo = new(); - private readonly Stack _redo = new(); - - // The clipboard is process-wide in the original (a real OS clipboard). There is no - // cross-platform clipboard in the BCL, so cut/copy/paste round-trip through this - // shared buffer instead -- fully functional within the app, which is what the - // managed TextBox's own tests exercise. - // TODO: bridge to the OS clipboard once a platform abstraction exists. - private static string s_clipboard = string.Empty; - - public RichTextObject(bool richTextMode, Size maximumSurface, IntPtr callbacks) - { - RichTextMode = richTextMode; - MaximumSurface = maximumSurface; - Callbacks = callbacks; - } - - public bool RichTextMode { get; } - public Size MaximumSurface { get; } - public IntPtr Callbacks { get; } - - public bool ReadOnly { get; set; } - public bool WordWrap { get; set; } = true; - public bool DetectUrls { get; set; } - public bool Oversample { get; set; } - public bool HasFocus { get; private set; } - public float Scale { get; set; } = 1.0f; - public int MaximumLength { get; set; } = int.MaxValue; - public bool AllowVerticalScroll { get; set; } - public bool AllowHorizontalScroll { get; set; } - - public int SelectionStart { get; private set; } - public int SelectionEnd { get; private set; } - - public string Text => _content.ToString(); - public int Length => _content.Length; - - public bool CanUndo => _undo.Count > 0; - - // ---- content ----------------------------------------------------------------- - - public void SetContent(string value) - { - PushUndo(); - _content.Clear(); - _content.Append(value ?? string.Empty); - ClampSelection(); - NotifyTextChanged(); - NotifyInvalidateContent(); - } - - public bool InsertText(string value) - { - if (ReadOnly || string.IsNullOrEmpty(value)) - return false; - - DeleteSelectionCore(); - - if (_content.Length + value.Length > MaximumLength) - { - NotifyMaxLengthExceeded(); - return false; - } - - PushUndo(); - _content.Insert(SelectionStart, value); - SelectionStart += value.Length; - SelectionEnd = SelectionStart; - NotifyTextChanged(); - NotifyInvalidateContent(); - return true; - } - - public bool DeleteSelection() - { - if (ReadOnly) - return false; - - PushUndo(); - if (!DeleteSelectionCore()) - return false; - - NotifyTextChanged(); - NotifyInvalidateContent(); - return true; - } - - private bool DeleteSelectionCore() - { - int start = Math.Min(SelectionStart, SelectionEnd); - int end = Math.Max(SelectionStart, SelectionEnd); - if (start == end) - return false; - - _content.Remove(start, end - start); - SelectionStart = SelectionEnd = start; - return true; - } - - public string GetSelectedText() - { - int start = Math.Min(SelectionStart, SelectionEnd); - int end = Math.Max(SelectionStart, SelectionEnd); - return start == end ? string.Empty : _content.ToString(start, end - start); - } - - public void SetSelectionRange(int start, int end) - { - SelectionStart = Math.Clamp(start, 0, _content.Length); - SelectionEnd = Math.Clamp(end, 0, _content.Length); - NotifySelectionChanged(); - } - - private void ClampSelection() - { - SelectionStart = Math.Clamp(SelectionStart, 0, _content.Length); - SelectionEnd = Math.Clamp(SelectionEnd, 0, _content.Length); - } - - // ---- clipboard --------------------------------------------------------------- - - public void Copy() => s_clipboard = GetSelectedText(); - - public bool Cut() - { - if (ReadOnly) - return false; - s_clipboard = GetSelectedText(); - return DeleteSelection(); - } - - public bool Paste() => InsertText(s_clipboard); - - // ---- undo / redo ------------------------------------------------------------- - - private void PushUndo() - { - _undo.Push(_content.ToString()); - _redo.Clear(); - } - - public bool Undo() - { - if (_undo.Count == 0) - return false; - - _redo.Push(_content.ToString()); - string previous = _undo.Pop(); - _content.Clear(); - _content.Append(previous); - ClampSelection(); - NotifyTextChanged(); - NotifyInvalidateContent(); - return true; - } - - public bool Redo() - { - if (_redo.Count == 0) - return false; - - _undo.Push(_content.ToString()); - string next = _redo.Pop(); - _content.Clear(); - _content.Append(next); - ClampSelection(); - NotifyTextChanged(); - NotifyInvalidateContent(); - return true; - } - - // ---- focus / caret ----------------------------------------------------------- - - public void SetFocus(bool gainingFocus) - { - HasFocus = gainingFocus; - NotifyShowCaret(gainingFocus); - } - - // ---- callback dispatch ------------------------------------------------------- - - private unsafe void Notify(int slot) - { - void* fn = ComVtable.Slot(Callbacks, slot); - if (fn != null) - ((delegate* unmanaged)fn)(Callbacks); - } - - private unsafe void Notify(int slot, int arg) - { - void* fn = ComVtable.Slot(Callbacks, slot); - if (fn != null) - ((delegate* unmanaged)fn)(Callbacks, arg); - } - - private unsafe void Notify(int slot, int a, int b) - { - void* fn = ComVtable.Slot(Callbacks, slot); - if (fn != null) - ((delegate* unmanaged)fn)(Callbacks, a, b); - } - - public void NotifyTextChanged() => Notify(SlotTextChanged); - public void NotifyInvalidateContent() => Notify(SlotInvalidateContent); - public void NotifySelectionChanged() => Notify(SlotSelectionChanged, SelectionStart, SelectionEnd); - public void NotifyMaxLengthExceeded() => Notify(SlotMaxLengthExceeded); - public void NotifyShowCaret(bool visible) => Notify(SlotShowCaret, visible ? 1 : 0); - public void NotifyLinkClicked(int start, int end) => Notify(SlotLinkClicked, start, end); - public void NotifySetScrollRange(int whichBar, int min, int extent, int viewExtent, int position) => NotifyScrollRange(whichBar, min, extent, viewExtent, position); - - private unsafe void NotifyScrollRange(int whichBar, int min, int extent, int viewExtent, int position) - { - void* fn = ComVtable.Slot(Callbacks, SlotSetScrollRange); - if (fn != null) - ((delegate* unmanaged)fn)(Callbacks, whichBar, min, extent, viewExtent, position); - } -} diff --git a/UIXrender/Subsystems/Text/SimpleTextApi.cs b/UIXrender/Subsystems/Text/SimpleTextApi.cs deleted file mode 100644 index cf03dad..0000000 --- a/UIXrender/Subsystems/Text/SimpleTextApi.cs +++ /dev/null @@ -1,96 +0,0 @@ -#if NETCOREAPP - -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.Drawing; -using Microsoft.Iris.Render.Interop.Text; -using Microsoft.Iris.Render.Interop.Win32; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// [UnmanagedCallersOnly] exports for the SpSimpleText* family in -// UIX/Microsoft/Iris/OS/NativeApi.cs -- measurement/rendering only, no editing. -public static unsafe class SimpleTextApi -{ - private sealed class SimpleTextObject(Size maximumSurface) - { - public Size MaximumSurface { get; } = maximumSurface; - } - - // The simple-text path *is* available: it's the measurement fast path, and this - // implementation provides measurement (approximately -- see TextMetrics). Reporting - // false here would push every caller onto the rich-text path for no benefit. - [UnmanagedCallersOnly(EntryPoint = "SpSimpleTextIsAvailable")] - public static int SpSimpleTextIsAvailable() => 1; - - [UnmanagedCallersOnly(EntryPoint = "SpSimpleTextBuildObject")] - public static HRESULT SpSimpleTextBuildObject(Size sizeMaximumSurface, HANDLE* hSto) - { - if (hSto == null) - return HRESULT.E_INVALIDARG; - - hSto->h = HandleTable.Alloc(new SimpleTextObject(sizeMaximumSurface)); - return HRESULT.S_OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpSimpleTextDestroyObject")] - public static void SpSimpleTextDestroyObject(HANDLE hSto) => HandleTable.Free(hSto.h); - - // Measures the run with the resolved font (real glyph advances + kerning, or the ratio - // fallback if no font is available) and produces a GlyphRun behind hGlyphRunInfo that - // SpRichTextRasterize can later draw. Fills the RasterizeRunPacket with the resulting - // geometry. - [UnmanagedCallersOnly(EntryPoint = "SpSimpleTextMeasure")] - public static HRESULT SpSimpleTextMeasure(HANDLE hSto, char* pszRef, short wAlignment, TextStyleData* textStyle, Size sizeConstraint, IntPtr* hGlyphRunInfo, RasterizeRunPacket* pRun) - { - if (hGlyphRunInfo == null || !HandleTable.TryGet(hSto.h, out SimpleTextObject _)) - return HRESULT.E_INVALIDARG; - - *hGlyphRunInfo = IntPtr.Zero; - - string text = NativeString.UniToString(pszRef) ?? string.Empty; - string face = textStyle != null ? NativeString.UniToString(textStyle->fontFace) : null; - float fontHeight = textStyle != null && textStyle->fontHeightPts > 0 ? textStyle->fontHeightPts : 12f; - Color color = textStyle != null ? textStyle->textColor : default; - - LoadedFont font = FontStore.Resolve(face); - bool wrap = sizeConstraint.width > 0; - Size measured = TextLayout.Measure(font, text, fontHeight, wrap, sizeConstraint.width); - - var run = new GlyphRun(font, text, fontHeight, color, measured); - *hGlyphRunInfo = HandleTable.Alloc(run); - - if (pRun != null) - { - int ascent = TextLayout.Ascent(font, fontHeight); - pRun->rcLayoutBounds = new Rectangle { x = 0, y = 0, width = measured.width, height = measured.height }; - pRun->rcfRenderBounds = new RectangleF { x = 0, y = 0, width = measured.width, height = measured.height }; - pRun->sizeRasterizeRun = measured; - pRun->sizeNatural = measured; - pRun->ascenderInset = ascent; - pRun->baselineInset = ascent; - pRun->lineNumber = 0; - pRun->clrText = color; - } - - return HRESULT.S_OK; - } - - // "Is this string measurable with this style" -- true whenever a font resolves (real - // or fallback), which is the condition under which SpSimpleTextMeasure will succeed. - [UnmanagedCallersOnly(EntryPoint = "SpSimpleTextMeasurePossible")] - public static HRESULT SpSimpleTextMeasurePossible(HANDLE hSto, char* pszRef, TextStyleData* textStyle, int* fPossible) - { - if (fPossible == null) - return HRESULT.E_INVALIDARG; - - string face = textStyle != null ? NativeString.UniToString(textStyle->fontFace) : null; - *fPossible = FontStore.Resolve(face) != null || textStyle != null ? 1 : 0; - return HRESULT.S_OK; - } -} - -#endif diff --git a/UIXrender/Subsystems/Text/TextLayout.cs b/UIXrender/Subsystems/Text/TextLayout.cs deleted file mode 100644 index 83a9152..0000000 --- a/UIXrender/Subsystems/Text/TextLayout.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// Real text measurement over a LoadedFont's actual glyph advances + kerning, with the -// ratio-based TextMetrics kept as the fallback for when no font resolves (headless with -// no fonts, an unrecognised face, etc.). Word wrapping is real either way; only the -// per-glyph width feeding it differs (real advances vs. an average). -internal static class TextLayout -{ - public static int LineHeight(LoadedFont font, float px) => - font != null ? (int)MathF.Ceiling(font.LineHeightPx(font.ScaleForPixelHeight(px))) : TextMetrics.LineHeight(px); - - public static int Ascent(LoadedFont font, float px) => - font != null ? (int)MathF.Round(font.AscentPx(font.ScaleForPixelHeight(px))) : TextMetrics.Ascent(px); - - public static Size Measure(LoadedFont font, string text, float px, bool wordWrap, int constraintWidth) - { - if (font == null) - return TextMetrics.Measure(text, px, wordWrap, constraintWidth); - - float scale = font.ScaleForPixelHeight(px); - int lineHeight = (int)MathF.Ceiling(font.LineHeightPx(scale)); - - if (string.IsNullOrEmpty(text)) - return new Size(0, lineHeight); - - int lines = 0; - int widest = 0; - - foreach (string paragraph in text.Split('\n')) - { - string line = paragraph.TrimEnd('\r'); - if (!wordWrap || constraintWidth <= 0) - { - widest = Math.Max(widest, MeasureWidth(font, scale, line)); - lines++; - } - else - { - foreach (int lineWidth in WrapWidths(font, scale, line, constraintWidth)) - { - widest = Math.Max(widest, lineWidth); - lines++; - } - } - } - - return new Size(widest, Math.Max(1, lines) * lineHeight); - } - - // Pixel width of one line, honouring kerning between adjacent glyphs. - public static int MeasureWidth(LoadedFont font, float scale, string line) - { - float width = 0f; - int previous = 0; - foreach (System.Text.Rune rune in line.EnumerateRunes()) - { - int cp = rune.Value; - if (previous != 0) - width += font.KerningPx(previous, cp, scale); - width += font.AdvancePx(cp, scale); - previous = cp; - } - return (int)MathF.Ceiling(width); - } - - // Greedy word wrap: fill each line with whole words until the next won't fit, then - // break. A single word wider than the constraint overflows onto its own line (matching - // the ratio fallback's behaviour) rather than being split mid-word. - private static IEnumerable WrapWidths(LoadedFont font, float scale, string paragraph, int constraintWidth) - { - if (paragraph.Length == 0) - { - yield return 0; - yield break; - } - - float spaceWidth = font.AdvancePx(' ', scale); - var current = new StringBuilder(); - int currentWidth = 0; - - foreach (string word in paragraph.Split(' ')) - { - int wordWidth = MeasureWidth(font, scale, word); - int withSpace = current.Length == 0 ? wordWidth : currentWidth + (int)MathF.Ceiling(spaceWidth) + wordWidth; - - if (current.Length != 0 && withSpace > constraintWidth) - { - yield return currentWidth; - current.Clear(); - current.Append(word); - currentWidth = wordWidth; - } - else - { - if (current.Length != 0) - current.Append(' '); - current.Append(word); - currentWidth = withSpace; - } - } - - yield return currentWidth; - } -} diff --git a/UIXrender/Subsystems/Text/TextMetrics.cs b/UIXrender/Subsystems/Text/TextMetrics.cs deleted file mode 100644 index b8936b4..0000000 --- a/UIXrender/Subsystems/Text/TextMetrics.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System; -using Microsoft.Iris.Render.Interop.Drawing; - -namespace Microsoft.Iris.Render.Subsystems.Text; - -// Text measurement for SpRichTextMeasure/SpSimpleTextMeasure/SpRichTextGetNaturalBounds. -// -// **This is the one genuinely approximate part of the whole UIXrender surface, and it is -// flagged rather than hidden.** Real measurement needs font loading plus glyph metrics -// and shaping. There is no such abstraction in Silk.NET, System.Drawing's is Windows-only -// (and a graphics API this project's dependency policy rules out), and adding a full text -// shaping stack (HarfBuzz/SixLabors.Fonts) is a much larger dependency decision than this -// pass should make unilaterally. -// -// So: metrics are derived from the requested font height using the ratios that hold for -// the overwhelming majority of Latin UI faces (Segoe UI, Verdana, Tahoma -- what Zune's -// markup actually asks for). Line height and baseline placement are close to exact; per- -// character advance is an average, so a measured string's *width* is approximate and will -// not match a real rasterizer. -// -// Consequence, stated plainly: layout driven by these numbers will be plausible but not -// pixel-accurate, and text will not currently rasterize at all (see -// RichTextApi.SpRichTextRasterize). Logged as the primary open question in -// logs/UIXrender/FullSurface.md. -// TODO: replace wholesale with a real font backend; do not build on these ratios. -internal static class TextMetrics -{ - // Typical for Latin UI faces: cap-to-em ratio ~0.7, ascent ~0.8 em, descent ~0.2 em, - // default line gap ~1.2 em. - private const float AverageAdvanceRatio = 0.55f; - private const float AscentRatio = 0.80f; - private const float LineHeightRatio = 1.20f; - - public static int LineHeight(float fontHeightPts) => (int)MathF.Ceiling(fontHeightPts * LineHeightRatio); - - public static int Ascent(float fontHeightPts) => (int)MathF.Round(fontHeightPts * AscentRatio); - - public static int AverageCharWidth(float fontHeightPts) => Math.Max(1, (int)MathF.Round(fontHeightPts * AverageAdvanceRatio)); - - // Measures `text` with optional word wrapping into `constraintWidth` (0 = unconstrained). - // Wrapping itself is real (break on whitespace, fall back to a hard break for a word - // longer than the line) -- only the per-character width feeding it is approximate. - public static Size Measure(string text, float fontHeightPts, bool wordWrap, int constraintWidth) - { - if (string.IsNullOrEmpty(text)) - return new Size(0, LineHeight(fontHeightPts)); - - int charWidth = AverageCharWidth(fontHeightPts); - int lineHeight = LineHeight(fontHeightPts); - - int maxCharsPerLine = wordWrap && constraintWidth > 0 - ? Math.Max(1, constraintWidth / charWidth) - : int.MaxValue; - - int lines = 0; - int widestLine = 0; - - foreach (string paragraph in text.Split('\n')) - { - string remaining = paragraph.TrimEnd('\r'); - do - { - int take = Math.Min(remaining.Length, maxCharsPerLine); - if (take < remaining.Length) - { - // Prefer breaking at the last space that fits. - int lastSpace = remaining.LastIndexOf(' ', Math.Max(0, take - 1)); - if (lastSpace > 0) - take = lastSpace; - } - - widestLine = Math.Max(widestLine, take * charWidth); - lines++; - remaining = remaining[take..].TrimStart(' '); - } - while (remaining.Length > 0); - } - - return new Size(widestLine, Math.Max(1, lines) * lineHeight); - } -} diff --git a/UIXrender/Subsystems/Tracing/TracingApi.cs b/UIXrender/Subsystems/Tracing/TracingApi.cs deleted file mode 100644 index 7a4c62f..0000000 --- a/UIXrender/Subsystems/Tracing/TracingApi.cs +++ /dev/null @@ -1,29 +0,0 @@ -#if NETCOREAPP - -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; - -namespace Microsoft.Iris.Render.Subsystems.Tracing; - -// [UnmanagedCallersOnly] exports matching the DllImport("UIXRender.dll") tracing -// declarations in UIX/Microsoft/Iris/OS/NativeApi.cs. -public static unsafe class TracingApi -{ - [UnmanagedCallersOnly(EntryPoint = "SpInitializeTracing")] - public static void SpInitializeTracing() => TracingState.Initialize(); - - [UnmanagedCallersOnly(EntryPoint = "SpUninitializeTracing")] - public static void SpUninitializeTracing() => TracingState.Uninitialize(); - - // The three `bool` params have no [MarshalAs] override in the original DllImport, so - // the CLR marshals each as the default 4-byte Win32 BOOL -- `int` here, not `byte`. - [UnmanagedCallersOnly(EntryPoint = "SpUpdateTraceSettings")] - public static void SpUpdateTraceSettings(char* debugTraceFile, char* writeLinePrefix, int sendOutputToDebugger, int showCategories, int timedWriteLines) => - TracingState.UpdateSettings(NativeString.UniToString(debugTraceFile), NativeString.UniToString(writeLinePrefix), sendOutputToDebugger != 0, showCategories != 0, timedWriteLines != 0); - - [UnmanagedCallersOnly(EntryPoint = "SpLogTrace")] - public static void SpLogTrace(char* categoryName, char* message, int indentLevel) => - TracingState.LogTrace(NativeString.UniToString(categoryName), NativeString.UniToString(message), indentLevel); -} - -#endif diff --git a/UIXrender/Subsystems/Tracing/TracingState.cs b/UIXrender/Subsystems/Tracing/TracingState.cs deleted file mode 100644 index d3f560f..0000000 --- a/UIXrender/Subsystems/Tracing/TracingState.cs +++ /dev/null @@ -1,66 +0,0 @@ -using System; -using System.IO; - -namespace Microsoft.Iris.Render.Subsystems.Tracing; - -// Real state for the whole tracing surface: SpInitializeTracing/SpUninitializeTracing -// (init flag) plus SpUpdateTraceSettings/SpLogTrace (settings + line formatting/sink). -internal static class TracingState -{ - public static bool IsInitialized { get; private set; } - - private static string s_debugTraceFile; - private static string s_writeLinePrefix = ""; - private static bool s_sendOutputToDebugger; - private static bool s_showCategories; - private static bool s_timedWriteLines; - private static readonly object s_fileLock = new(); - - public static void Initialize() => IsInitialized = true; - public static void Uninitialize() => IsInitialized = false; - - public static void UpdateSettings(string debugTraceFile, string writeLinePrefix, bool sendOutputToDebugger, bool showCategories, bool timedWriteLines) - { - s_debugTraceFile = debugTraceFile; - s_writeLinePrefix = writeLinePrefix ?? ""; - s_sendOutputToDebugger = sendOutputToDebugger; - s_showCategories = showCategories; - s_timedWriteLines = timedWriteLines; - } - - public static void LogTrace(string categoryName, string message, int indentLevel) - { - if (!IsInitialized) - return; - - var line = new System.Text.StringBuilder(); - line.Append(s_writeLinePrefix); - if (s_timedWriteLines) - line.Append('[').Append(DateTime.Now.ToString("HH:mm:ss.fff")).Append("] "); - if (s_showCategories && !string.IsNullOrEmpty(categoryName)) - line.Append('[').Append(categoryName).Append("] "); - line.Append(' ', indentLevel * 2); - line.Append(message); - - string text = line.ToString(); - - if (s_sendOutputToDebugger) - Console.Error.WriteLine(text); - - if (!string.IsNullOrEmpty(s_debugTraceFile)) - { - lock (s_fileLock) - { - try - { - File.AppendAllText(s_debugTraceFile, text + Environment.NewLine); - } - catch (IOException) - { - // Best-effort trace sink -- matches the original's fire-and-forget - // SpLogTrace, which has no HRESULT to report failure through either. - } - } - } - } -} diff --git a/UIXrender/Subsystems/Xml/XmlLiteApi.cs b/UIXrender/Subsystems/Xml/XmlLiteApi.cs deleted file mode 100644 index 0f62a34..0000000 --- a/UIXrender/Subsystems/Xml/XmlLiteApi.cs +++ /dev/null @@ -1,189 +0,0 @@ -#if NETCOREAPP - -using System; -using System.Runtime.InteropServices; -using System.Text; -using System.Xml; -using Microsoft.Iris.Interop; -using Microsoft.Iris.Render.Engine; -using Microsoft.Iris.Render.Interop; -using Microsoft.Iris.Render.Interop.XmlLite; - -namespace Microsoft.Iris.Render.Subsystems.Xml; - -// [UnmanagedCallersOnly] exports for the SpXmlLite* family in UIX/Microsoft/Iris/OS/NativeApi.cs. -// -// Two behaviours below are read off the *caller* (UIX/Microsoft/Iris/OS/NativeXmlReader.cs) -// rather than assumed, because getting either backwards would produce an infinite loop or -// a silently truncated parse: -// * `SpXmlLiteRead` / `SpXmlLiteMoveToFirstAttribute` / `SpXmlLiteMoveToNextAttribute` -// are each tested with `SUCCEEDED(...)`, so "nothing more to read" must be reported as -// a **failing** HRESULT, not S_FALSE (which SUCCEEDED would accept as "keep going"). -// * the `length` passed to `SpXmlLiteCreateXmlReader` is a **byte** count, not a -// character count: NativeXmlReader's string overload passes `content.Length * 2` over -// a pinned UTF-16 string, and its Resource overload passes a raw file buffer length. -public static unsafe class XmlLiteApi -{ - private static uint OK => (uint)HRESULT.S_OK.hr; - private static uint Fail => unchecked((uint)HRESULT.E_FAIL.hr); - private static uint InvalidArg => unchecked((uint)HRESULT.E_INVALIDARG.hr); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteCreateXmlReader")] - public static uint SpXmlLiteCreateXmlReader(IntPtr buffer, int length, int isFragment, IntPtr* xmlReader) - { - if (xmlReader == null) - return InvalidArg; - - *xmlReader = IntPtr.Zero; - if (buffer == IntPtr.Zero || length <= 0) - return InvalidArg; - - try - { - string text = DecodeBuffer((byte*)buffer, length); - *xmlReader = HandleTable.Alloc(XmlLiteReader.Create(text, isFragment != 0)); - return OK; - } - catch (XmlException) - { - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteDeleteXmlReader")] - public static void SpXmlLiteDeleteXmlReader(IntPtr xmlReader) => HandleTable.Free(xmlReader); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteRead")] - public static uint SpXmlLiteRead(IntPtr xmlReader, NativeXmlNodeType* nodeType) - { - if (nodeType == null || !HandleTable.TryGet(xmlReader, out XmlLiteReader reader)) - return InvalidArg; - - try - { - // Failure at EOF is deliberate -- see the file comment. - return reader.Read(out NativeXmlNodeType type) ? Assign(nodeType, type, OK) : Assign(nodeType, type, Fail); - } - catch (XmlException) - { - *nodeType = NativeXmlNodeType.None; - return Fail; - } - } - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteMoveToFirstAttribute")] - public static uint SpXmlLiteMoveToFirstAttribute(IntPtr xmlReader) => - HandleTable.TryGet(xmlReader, out XmlLiteReader reader) - ? (reader.MoveToFirstAttribute() ? OK : Fail) - : InvalidArg; - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteMoveToNextAttribute")] - public static uint SpXmlLiteMoveToNextAttribute(IntPtr xmlReader) => - HandleTable.TryGet(xmlReader, out XmlLiteReader reader) - ? (reader.MoveToNextAttribute() ? OK : Fail) - : InvalidArg; - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteIsEmptyElement")] - public static int SpXmlLiteIsEmptyElement(IntPtr xmlReader) => - HandleTable.TryGet(xmlReader, out XmlLiteReader reader) && reader.IsEmptyElement ? 1 : 0; - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetQualifiedName")] - public static uint SpXmlLiteGetQualifiedName(IntPtr xmlReader, IntPtr* name, uint* length) => - ReturnString(xmlReader, name, length, static r => r.QualifiedName); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetLocalName")] - public static uint SpXmlLiteGetLocalName(IntPtr xmlReader, IntPtr* name, uint* length) => - ReturnString(xmlReader, name, length, static r => r.LocalName); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetPrefix")] - public static uint SpXmlLiteGetPrefix(IntPtr xmlReader, IntPtr* prefix, uint* length) => - ReturnString(xmlReader, prefix, length, static r => r.Prefix); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetValue")] - public static uint SpXmlLiteGetValue(IntPtr xmlReader, IntPtr* value, uint* length) => - ReturnString(xmlReader, value, length, static r => r.Value); - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetLineNumber")] - public static uint SpXmlLiteGetLineNumber(IntPtr xmlReader, uint* lineNumber) - { - if (lineNumber == null || !HandleTable.TryGet(xmlReader, out XmlLiteReader reader)) - return InvalidArg; - *lineNumber = reader.LineNumber; - return OK; - } - - [UnmanagedCallersOnly(EntryPoint = "SpXmlLiteGetLinePosition")] - public static uint SpXmlLiteGetLinePosition(IntPtr xmlReader, uint* linePosition) - { - if (linePosition == null || !HandleTable.TryGet(xmlReader, out XmlLiteReader reader)) - return InvalidArg; - *linePosition = reader.LinePosition; - return OK; - } - - // ---- helpers --------------------------------------------------------------------- - - private static uint Assign(NativeXmlNodeType* target, NativeXmlNodeType value, uint result) - { - *target = value; - return result; - } - - // The returned pointer must stay valid until at least the caller's next call, and the - // managed side never frees it (NativeApi.PtrToStringUni just reads it) -- so these go - // through the intern pool, which also keeps repeated element/attribute names from - // allocating anew on every node. - private static uint ReturnString(IntPtr xmlReader, IntPtr* target, uint* length, Func select) - { - if (target == null || length == null || !HandleTable.TryGet(xmlReader, out XmlLiteReader reader)) - return InvalidArg; - - string value = select(reader) ?? string.Empty; - *target = (IntPtr)NativeString.InternUni(value); - *length = (uint)value.Length; - return OK; - } - - // The buffer is bytes of unknown encoding: NativeXmlReader feeds it either a pinned - // UTF-16 string (no BOM) or a raw file buffer (typically UTF-8, possibly with a BOM). - // Real XmlLite sniffs the encoding itself, so this does the same rather than assuming - // one: BOM first, then the "every second byte is zero" pattern that distinguishes - // BOM-less UTF-16 ASCII text, else UTF-8. Documented assumption -- see - // logs/UIXrender/FullSurface.md. - private static string DecodeBuffer(byte* buffer, int byteLength) - { - var bytes = new ReadOnlySpan(buffer, byteLength); - - if (byteLength >= 2) - { - if (bytes[0] == 0xFF && bytes[1] == 0xFE) - return Encoding.Unicode.GetString(bytes[2..]); - if (bytes[0] == 0xFE && bytes[1] == 0xFF) - return Encoding.BigEndianUnicode.GetString(bytes[2..]); - } - - if (byteLength >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF) - return Encoding.UTF8.GetString(bytes[3..]); - - if (LooksLikeUtf16LittleEndian(bytes)) - return Encoding.Unicode.GetString(bytes); - - return Encoding.UTF8.GetString(bytes); - } - - private static bool LooksLikeUtf16LittleEndian(ReadOnlySpan bytes) - { - if (bytes.Length < 2 || (bytes.Length & 1) != 0) - return false; - - int sampled = Math.Min(bytes.Length, 32); - for (int i = 1; i < sampled; i += 2) - { - if (bytes[i] != 0) - return false; - } - return true; - } -} - -#endif diff --git a/UIXrender/Subsystems/Xml/XmlLiteReader.cs b/UIXrender/Subsystems/Xml/XmlLiteReader.cs deleted file mode 100644 index e11f883..0000000 --- a/UIXrender/Subsystems/Xml/XmlLiteReader.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System; -using System.IO; -using System.Xml; -using Microsoft.Iris.Render.Interop.XmlLite; - -namespace Microsoft.Iris.Render.Subsystems.Xml; - -// Backs the SpXmlLite* family (UIX/Microsoft/Iris/OS/NativeApi.cs). The original was a -// thin wrapper over Windows' XmlLite COM reader; this is the same *contract* over -// System.Xml.XmlReader, which is in-box, cross-platform and behaviourally equivalent for -// the pull-parsing subset the surface exposes (read a node, inspect name/prefix/value, -// walk attributes, report line/position). -// -// One real semantic difference to be aware of and handled here, not papered over: XmlLite -// reports attributes as nodes you move to with MoveToFirst/NextAttribute and then read -// via the *same* GetLocalName/GetValue accessors, and System.Xml.XmlReader works exactly -// that way too -- so the mapping is direct. What is *not* direct is IsEmptyElement, which -// XmlReader only reports on the element node itself; it's captured on each read rather -// than queried lazily, so it stays correct after moving to an attribute. -internal sealed class XmlLiteReader : IDisposable -{ - private readonly XmlReader _reader; - private readonly IXmlLineInfo _lineInfo; - - private XmlLiteReader(XmlReader reader) - { - _reader = reader; - _lineInfo = reader as IXmlLineInfo; - } - - public bool IsEmptyElement { get; private set; } - - public static XmlLiteReader Create(string text, bool isFragment) - { - var settings = new XmlReaderSettings - { - // A "fragment" has no single root element; ConformanceLevel.Fragment is - // exactly XmlLite's isFragment flag. - ConformanceLevel = isFragment ? ConformanceLevel.Fragment : ConformanceLevel.Document, - DtdProcessing = DtdProcessing.Ignore, - IgnoreWhitespace = false, - CloseInput = true, - }; - - return new XmlLiteReader(XmlReader.Create(new StringReader(text), settings)); - } - - public bool Read(out NativeXmlNodeType nodeType) - { - if (!_reader.Read()) - { - nodeType = NativeXmlNodeType.None; - IsEmptyElement = false; - return false; - } - - IsEmptyElement = _reader.NodeType == XmlNodeType.Element && _reader.IsEmptyElement; - nodeType = Map(_reader.NodeType); - return true; - } - - public bool MoveToFirstAttribute() => _reader.MoveToFirstAttribute(); - - public bool MoveToNextAttribute() => _reader.MoveToNextAttribute(); - - public string LocalName => _reader.LocalName; - public string Prefix => _reader.Prefix; - public string QualifiedName => _reader.Name; - public string Value => _reader.Value; - - public uint LineNumber => (uint)(_lineInfo?.LineNumber ?? 0); - public uint LinePosition => (uint)(_lineInfo?.LinePosition ?? 0); - - // NativeXmlNodeType's values are System.Xml.XmlNodeType's own numbering minus the - // members Iris doesn't use (verified against UIX/Microsoft/Iris/OS/NativeXmlNodeType.cs), - // so this could be a cast -- it's written out so an unmapped node type degrades to - // None instead of producing a value the managed side has no case for. - private static NativeXmlNodeType Map(XmlNodeType type) => type switch - { - XmlNodeType.Element => NativeXmlNodeType.Element, - XmlNodeType.Attribute => NativeXmlNodeType.Attribute, - XmlNodeType.Text => NativeXmlNodeType.Text, - XmlNodeType.CDATA => NativeXmlNodeType.CDATA, - XmlNodeType.ProcessingInstruction => NativeXmlNodeType.ProcessingInstruction, - XmlNodeType.Comment => NativeXmlNodeType.Comment, - XmlNodeType.DocumentType => NativeXmlNodeType.DocumentType, - XmlNodeType.Whitespace or XmlNodeType.SignificantWhitespace => NativeXmlNodeType.Whitespace, - XmlNodeType.EndElement => NativeXmlNodeType.EndElement, - XmlNodeType.XmlDeclaration => NativeXmlNodeType.XmlDeclaration, - _ => NativeXmlNodeType.None, - }; - - public void Dispose() => _reader.Dispose(); -} diff --git a/UIXrender/UIXrender.csproj b/UIXrender/UIXrender.csproj index f0da195..3f70386 100644 --- a/UIXrender/UIXrender.csproj +++ b/UIXrender/UIXrender.csproj @@ -8,15 +8,6 @@ true - - true - - - Shared - - diff --git a/UIXsup/DebugState.cs b/UIXsup/DebugState.cs deleted file mode 100644 index 4199d6a..0000000 --- a/UIXsup/DebugState.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace Microsoft.Iris.Support; - -internal static class DebugState -{ - private static readonly byte[] s_categoryLevels = new byte[(int)DebugCategory.TotalCount]; - - public static bool TimedWriteLines { get; set; } - public static string WriteLinePrefix { get; set; } = string.Empty; - - public static byte GetCategoryLevel(DebugCategory category) - { - int index = (int)category; - return (uint)index < (uint)s_categoryLevels.Length ? s_categoryLevels[index] : (byte)0; - } - - public static void SetCategoryLevel(DebugCategory category, byte level) - { - int index = (int)category; - if ((uint)index < (uint)s_categoryLevels.Length) - s_categoryLevels[index] = level; - } -} diff --git a/UIXsup/Interop/DebugApi.cs b/UIXsup/Interop/DebugApi.cs deleted file mode 100644 index 321e370..0000000 --- a/UIXsup/Interop/DebugApi.cs +++ /dev/null @@ -1,37 +0,0 @@ -using System; -using System.Runtime.InteropServices; -using Microsoft.Iris.Interop; - -namespace Microsoft.Iris.Support.Interop; - -// [UnmanagedCallersOnly] exports matching the 5 DllImport("UIXsup.dll") declarations in -// the already-shipped managed callers (eDebugApi.cs / DebugHelpers.cs) field-for-field. -// See logs/UIXrender/UIXsup.md for why parameters are byte*/int here instead of -// string/bool (ANSI marshaling, blittability) and for the DebugDisplayErrorStack scope -// gap noted below. -public static unsafe class DebugApi -{ - [UnmanagedCallersOnly(EntryPoint = "DebugDisplayErrorStack")] - public static int DebugDisplayErrorStack(byte* stMessage, byte* filename, int line, byte* title, byte* stackTrace) - { - string timestamp = DebugState.TimedWriteLines ? $"[{DateTime.Now:HH:mm:ss.fff}] " : string.Empty; - Console.Error.WriteLine( - $"{timestamp}{DebugState.WriteLinePrefix}{NativeString.AnsiToString(title)}: {NativeString.AnsiToString(stMessage)} ({NativeString.AnsiToString(filename)}:{line})\n{NativeString.AnsiToString(stackTrace)}"); - - // TODO: no interactive dialog UI yet (see logs/UIXrender/UIXsup.md) -- always - // reports "don't break". - return 0; - } - - [UnmanagedCallersOnly(EntryPoint = "DebugSetTimedWriteLines")] - public static void DebugSetTimedWriteLines(int fEnabled) => DebugState.TimedWriteLines = fEnabled != 0; - - [UnmanagedCallersOnly(EntryPoint = "DebugSetWriteLinePrefix")] - public static void DebugSetWriteLinePrefix(byte* stPrefix) => DebugState.WriteLinePrefix = NativeString.AnsiToString(stPrefix) ?? string.Empty; - - [UnmanagedCallersOnly(EntryPoint = "DebugGetCategoryLevel")] - public static byte DebugGetCategoryLevel(DebugCategory cat) => DebugState.GetCategoryLevel(cat); - - [UnmanagedCallersOnly(EntryPoint = "DebugSetCategoryLevel")] - public static void DebugSetCategoryLevel(DebugCategory cat, byte level) => DebugState.SetCategoryLevel(cat, level); -} diff --git a/UIXsup/UIXsup.csproj b/UIXsup/UIXsup.csproj index 18430de..8673abc 100644 --- a/UIXsup/UIXsup.csproj +++ b/UIXsup/UIXsup.csproj @@ -6,13 +6,6 @@ Microsoft.Iris.Support true true - - true - - - Shared