More engine API work

This commit is contained in:
Yoshi Askharoun
2026-07-22 18:34:57 -05:00
parent f8e3051d07
commit 679d2bb902
58 changed files with 5540 additions and 80 deletions
+56 -4
View File
@@ -6,11 +6,12 @@ on:
jobs:
build:
# UIXrender/UIXsup are implemented cross-platform (Silk.NET, no Windows-specific
# APIs), but NativeAOT can't cross-compile Linux->Windows, and the P/Invoke-based
# UIXrender/UIXsup are implemented cross-platform (the only Windows-specific code is
# explicitly `#if WINDOWS` gated -- DPI, registry-change notification, cursor
# metrics), but NativeAOT can't cross-compile Linux->Windows, and the P/Invoke-based
# managed consumers we need to stay compatible with (UIX.dll et al) only run on
# Windows today -- so Windows is what Phase 0 verifies against here. Other RIDs can
# get their own job later once there's something worth publishing them for.
# Windows today -- so Windows is what the interop spike verifies against. The linux
# job below covers what doesn't need Windows, including export-surface completeness.
runs-on: windows-latest
steps:
@@ -26,6 +27,12 @@ jobs:
- name: Build solution
run: dotnet build MicrosoftIris.sln -c Release --no-restore
# Exercises EngineService directly (no P/Invoke, no publish needed) -- runs on
# any platform since it's pure managed code; on windows-latest here just because
# that's the only job today, not because it needs Windows specifically.
- name: Run EngineService managed-direct tests
run: dotnet run --project Tests/UIXrender.Engine.Tests/UIXrender.Engine.Tests.csproj -c Release
- name: Publish UIXrender (NativeAOT, win-x64)
run: dotnet publish UIXrender/UIXrender.csproj -c Release -f net8.0 -r win-x64 --self-contained -p:PublishAot=true
@@ -49,3 +56,48 @@ jobs:
Copy-Item $dll.FullName -Destination $testExe.DirectoryName
& $testExe.FullName
if ($LASTEXITCODE -ne 0) { throw "Interop spike failed with exit code $LASTEXITCODE" }
linux:
# Verifies the two things that don't need Windows and that the windows job can't
# easily check: that the managed subsystem models actually behave, and that the
# NativeAOT-published library exports *exactly* the set of entry points the managed
# consumers declare -- no missing export (a runtime EntryPointNotFoundException
# waiting to happen) and no stray one.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- name: Run UIXrender subsystem tests
run: dotnet run --project Tests/UIXrender.Engine.Tests/UIXrender.Engine.Tests.csproj -c Release -f net8.0
- name: Publish UIXrender (NativeAOT, linux-x64)
run: dotnet publish UIXrender/UIXrender.csproj -c Release -f net8.0 -r linux-x64 --self-contained -p:PublishAot=true
- name: Verify exported surface matches the managed declarations
shell: bash
run: |
set -euo pipefail
# Every Sp* entry point the managed consumers P/Invoke for.
grep -hoP 'extern\s+.*?\bSp\w+\s*\(' \
UIX/Microsoft/Iris/OS/NativeApi.cs \
UIX.RenderApi/Microsoft/Iris/Render/Extensions/ExtensionsApi.cs \
UIX.RenderApi/Microsoft/Iris/Render/Internal/FormApi.cs \
UIX.RenderApi/Microsoft/Iris/Render/Protocol/EngineApi.cs \
| grep -oP 'Sp\w+(?=\s*\()' | sort -u > /tmp/declared.txt
SO=$(find UIXrender/bin -name UIXrender.so -path '*publish*' | head -1)
[ -n "$SO" ] || { echo "Published UIXrender.so not found"; exit 1; }
nm -D --defined-only "$SO" | grep -oP '\bSp\w+' | sort -u > /tmp/exported.txt
echo "declared: $(wc -l < /tmp/declared.txt) exported: $(wc -l < /tmp/exported.txt)"
if ! diff -u /tmp/declared.txt /tmp/exported.txt; then
echo "::error::Exported surface does not match the managed declarations (- = declared but missing, + = exported but undeclared)"
exit 1
fi
echo "Export surface matches exactly."
+15
View File
@@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXInterop", "UIXInterop\UI
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXrender.Interop.Tests", "Tests\UIXrender.Interop.Tests\UIXrender.Interop.Tests.csproj", "{D8DA0828-9ACC-430F-9A12-E09ECE63823C}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXrender.Engine.Tests", "Tests\UIXrender.Engine.Tests\UIXrender.Engine.Tests.csproj", "{568E8DCD-CAB3-499B-A264-C62B1E1082B7}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -132,6 +134,18 @@ Global
{D8DA0828-9ACC-430F-9A12-E09ECE63823C}.Release|x64.Build.0 = Release|x64
{D8DA0828-9ACC-430F-9A12-E09ECE63823C}.Release|x86.ActiveCfg = Release|x86
{D8DA0828-9ACC-430F-9A12-E09ECE63823C}.Release|x86.Build.0 = Release|x86
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|x64.ActiveCfg = Debug|x64
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|x64.Build.0 = Debug|x64
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|x86.ActiveCfg = Debug|x86
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Debug|x86.Build.0 = Debug|x86
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|Any CPU.Build.0 = Release|Any CPU
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|x64.ActiveCfg = Release|x64
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|x64.Build.0 = Release|x64
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|x86.ActiveCfg = Release|x86
{568E8DCD-CAB3-499B-A264-C62B1E1082B7}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -140,6 +154,7 @@ Global
{283532A3-A744-4C1C-960C-0B4FE9AF98C9} = {1F0A2AD3-81C3-4A53-8AEF-DC67ABC63183}
{96FB9772-5619-47AD-93FC-F26319EC6BB1} = {1F0A2AD3-81C3-4A53-8AEF-DC67ABC63183}
{D8DA0828-9ACC-430F-9A12-E09ECE63823C} = {1F0A2AD3-81C3-4A53-8AEF-DC67ABC63183}
{568E8DCD-CAB3-499B-A264-C62B1E1082B7} = {1F0A2AD3-81C3-4A53-8AEF-DC67ABC63183}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {25300402-D4C7-4F92-841D-894A842FF055}
+199
View File
@@ -0,0 +1,199 @@
using System;
using System.Threading;
using Microsoft.Iris.Render.Engine;
using Microsoft.Iris.Render.Interop;
using Microsoft.Iris.Render.Interop.Extensions;
using Microsoft.Iris.Render.Interop.Protocol;
using Microsoft.Iris.Render.Interop.XmlLite;
using Microsoft.Iris.Render.Subsystems.Assets;
using Microsoft.Iris.Render.Subsystems.Lists;
using Microsoft.Iris.Render.Subsystems.Schema;
using Microsoft.Iris.Render.Subsystems.Text;
using Microsoft.Iris.Render.Subsystems.Xml;
// Exercises Microsoft.Iris.Render.Engine.EngineService directly (no P/Invoke, no
// published native DLL needed) -- unlike Tests/UIXrender.Interop.Tests, this can
// actually run cross-platform since EngineService is entirely managed/pointer-free.
// See logs/UIXrender/EngineCore.md.
int failures = 0;
void Check(bool condition, string message)
{
Console.WriteLine((condition ? "PASS: " : "FAIL: ") + message);
if (!condition)
failures++;
}
var contextId = new ContextID(99);
var callbackInvoked = new ManualResetEventSlim(false);
uint observedSource = 0;
RENDERHANDLE observedHandle = default;
byte[]? observedData = null;
BufferReceivedHandler handler = (source, bufferHandle, flags, data) =>
{
observedSource = source.value;
observedHandle = bufferHandle;
observedData = data.ToArray();
callbackInvoked.Set();
};
IRenderThreadHandle thread = EngineService.StartRenderThread(contextId, handler);
Check(thread.ContextId == contextId, "StartRenderThread returns a handle for the requested context");
bool signaledOnStart = callbackInvoked.Wait(TimeSpan.FromSeconds(5));
Check(signaledOnStart, "render thread invoked the handler once on start");
callbackInvoked.Reset();
byte[] payload = { 1, 2, 3, 4 };
var sourceContext = new ContextID(7);
var bufferHandle = new RENDERHANDLE(123);
HRESULT hr = EngineService.SendBuffer(sourceContext, contextId, bufferHandle, BufferFlags.CopyData, payload);
Check(hr.IsSuccess(), $"SendBuffer to a registered context succeeds (hr=0x{hr.hr:X8})");
bool signaledOnSend = callbackInvoked.Wait(TimeSpan.FromSeconds(5));
Check(signaledOnSend, "handler observed a buffer sent via SendBuffer");
Check(observedSource == 7, $"handler observed the correct source context (got {observedSource})");
Check(observedHandle == bufferHandle, "handler observed the correct buffer handle");
Check(observedData is { Length: 4 } d && d[0] == 1 && d[3] == 4, "handler observed the correct payload bytes");
// Unregistering: sending to a torn-down context should fail cleanly, not throw/hang.
thread.Dispose();
HRESULT hrAfterDispose = EngineService.SendBuffer(sourceContext, contextId, bufferHandle, BufferFlags.CopyData, payload);
Check(hrAfterDispose.IsError(), $"SendBuffer to a disposed/unregistered context fails cleanly (hr=0x{hrAfterDispose.hr:X8})");
// ---------------------------------------------------------------------------------
// Subsystem models added by the full-surface pass (logs/UIXrender/FullSurface.md).
// The exports wrapping these are [UnmanagedCallersOnly] and so uncallable from C#;
// these exercise the real logic behind them via InternalsVisibleTo.
// ---------------------------------------------------------------------------------
Console.WriteLine("\n-- UIXList --");
var list = new UIXList();
Check(list.Add(UIXVariant.FromObject(10)) == 1, "UIXList.Add returns the new count");
list.Add(UIXVariant.FromObject(20));
list.Add(UIXVariant.FromObject(30));
Check(list.Count == 3, "UIXList tracks count across adds");
Check(list.IndexOf(UIXVariant.FromObject(20)) == 1, "UIXList.IndexOf finds a variant by value");
Check(list.Move(0, 2), "UIXList.Move succeeds for valid indices");
Check(list.TryGet(2, out UIXVariant moved) && moved.AsInt32 == 10, "UIXList.Move puts the item at the new index");
Check(!list.Move(0, 99), "UIXList.Move rejects an out-of-range index");
Check(list.IsItemAvailable(0), "UIXList reports a resident item as available");
Check(list.RemoveAt(0) && list.Count == 2, "UIXList.RemoveAt removes and updates count");
list.Clear();
Check(list.Count == 0, "UIXList.Clear empties the list");
Console.WriteLine("\n-- WaveParser --");
// Minimal 8-bit mono PCM RIFF/WAVE file: 4 sample bytes.
byte[] wav =
[
.. "RIFF"u8, 0x28, 0, 0, 0, .. "WAVE"u8,
.. "fmt "u8, 16, 0, 0, 0,
1, 0, // PCM
1, 0, // mono
0x44, 0xAC, 0, 0, // 44100 Hz
0x44, 0xAC, 0, 0, // avg bytes/sec
1, 0, // block align
8, 0, // bits per sample
.. "data"u8, 4, 0, 0, 0,
0x11, 0x22, 0x33, 0x44,
];
Check(WaveParser.TryParse(wav, out SoundHeader wavHeader, out byte[] wavSamples), "WaveParser parses a well-formed PCM RIFF/WAVE buffer");
Check(wavHeader.samplesPerSec == 44100, $"WaveParser reads the sample rate (got {wavHeader.samplesPerSec})");
Check(wavHeader.channels == 1 && wavHeader.bitsPerSample == 8, "WaveParser reads channel count and bit depth");
Check(wavSamples.Length == 4 && wavSamples[0] == 0x11 && wavSamples[3] == 0x44, "WaveParser returns the data chunk's bytes");
Check(!WaveParser.TryParse("NOTARIFF"u8, out _, out _), "WaveParser rejects a non-RIFF buffer");
Console.WriteLine("\n-- XmlLiteReader --");
using (XmlLiteReader xml = XmlLiteReader.Create("<a x='1'><b/>text</a>", isFragment: false))
{
Check(xml.Read(out NativeXmlNodeType n1) && n1 == NativeXmlNodeType.Element && xml.LocalName == "a", "XmlLiteReader reads the root element");
Check(xml.MoveToFirstAttribute() && xml.LocalName == "x" && xml.Value == "1", "XmlLiteReader walks to an attribute and reads its value");
Check(xml.Read(out NativeXmlNodeType n2) && n2 == NativeXmlNodeType.Element && xml.IsEmptyElement, "XmlLiteReader reports an empty element");
Check(xml.Read(out NativeXmlNodeType n3) && n3 == NativeXmlNodeType.Text && xml.Value == "text", "XmlLiteReader reads a text node");
Check(xml.Read(out NativeXmlNodeType n4) && n4 == NativeXmlNodeType.EndElement, "XmlLiteReader reads the end element");
Check(!xml.Read(out _), "XmlLiteReader reports end-of-stream (the caller's SUCCEEDED() gate)");
Check(xml.LineNumber > 0, "XmlLiteReader reports line information");
}
Console.WriteLine("\n-- RichTextObject --");
var rich = new RichTextObject(richTextMode: false, new Microsoft.Iris.Render.Interop.Drawing.Size(400, 100), IntPtr.Zero);
rich.SetContent("hello");
Check(rich.Text == "hello" && rich.Length == 5, "RichTextObject.SetContent stores content");
rich.SetSelectionRange(0, 5);
Check(rich.GetSelectedText() == "hello", "RichTextObject returns the selected text");
rich.Copy();
rich.SetSelectionRange(5, 5);
Check(rich.Paste() && rich.Text == "hellohello", "RichTextObject copy/paste round-trips through the clipboard");
Check(rich.CanUndo && rich.Undo() && rich.Text == "hello", "RichTextObject.Undo restores the previous content");
Check(rich.Redo() && rich.Text == "hellohello", "RichTextObject.Redo reapplies the undone edit");
rich.ReadOnly = true;
Check(!rich.InsertText("x"), "RichTextObject refuses edits while read-only");
rich.ReadOnly = false;
rich.MaximumLength = 10;
Check(!rich.InsertText("overflow"), "RichTextObject enforces MaximumLength");
rich.SetSelectionRange(0, 5);
Check(rich.DeleteSelection() && rich.Text == "hello", "RichTextObject.DeleteSelection removes the selected range");
Console.WriteLine("\n-- TextMetrics --");
Microsoft.Iris.Render.Interop.Drawing.Size empty = TextMetrics.Measure("", 12f, false, 0);
Check(empty.width == 0 && empty.height > 0, "TextMetrics gives empty text zero width but one line of height");
Microsoft.Iris.Render.Interop.Drawing.Size oneLine = TextMetrics.Measure("hello world", 12f, false, 0);
Check(oneLine.width > 0 && oneLine.height == TextMetrics.LineHeight(12f), "TextMetrics measures unwrapped text as a single line");
Microsoft.Iris.Render.Interop.Drawing.Size wrapped = TextMetrics.Measure("hello world this is a longer string", 12f, true, 40);
Check(wrapped.height > oneLine.height, "TextMetrics wraps into multiple lines when constrained");
Microsoft.Iris.Render.Interop.Drawing.Size twoParagraphs = TextMetrics.Measure("a\nb", 12f, false, 0);
Check(twoParagraphs.height == TextMetrics.LineHeight(12f) * 2, "TextMetrics counts explicit newlines as separate lines");
Console.WriteLine("\n-- SchemaRegistration (native reflection over CLR types) --");
var schema = new SchemaRegistration();
schema.Add(typeof(SchemaProbe));
schema.Add(typeof(SchemaProbeKind));
Check(schema.Types.Count == 1 && schema.Enums.Count == 1, "SchemaRegistration separates types from enums");
TypeSchema probe = schema.Types[0];
Check(probe.Name == nameof(SchemaProbe), "TypeSchema exposes the CLR type name");
Check(Array.Exists(probe.Properties, p => p.Name == nameof(SchemaProbe.Value)), "TypeSchema enumerates public properties");
Check(Array.Exists(probe.Methods, m => m.Name == nameof(SchemaProbe.Add)), "TypeSchema enumerates public methods");
Check(probe.Constructors.Length == 1, "TypeSchema enumerates public constructors");
EnumSchema kind = schema.Enums[0];
Check(kind.Names.Length == 2 && kind.Values[1] == 5, "EnumSchema reads names and (non-contiguous) values");
Check(!kind.IsFlags, "EnumSchema reports non-flags enums correctly");
Console.WriteLine("\n-- UIXVariant round-trip --");
Check(UIXVariant.FromObject(42).AsInt32 == 42, "UIXVariant round-trips Int32");
Check(Math.Abs(UIXVariant.FromObject(1.5f).AsSingle - 1.5f) < float.Epsilon, "UIXVariant round-trips Single");
Check(Math.Abs(UIXVariant.FromObject(2.5d).AsDouble - 2.5d) < double.Epsilon, "UIXVariant round-trips Double");
Check(UIXVariant.FromObject(true).AsBool, "UIXVariant round-trips Bool");
Check(UIXVariant.FromObject(7L).ToObject() is long and 7L, "UIXVariant.ToObject returns the tagged CLR type");
Check(UIXVariant.Empty.type == VariantType.Empty, "UIXVariant.Empty carries the Empty tag");
Console.WriteLine("\n-- Remote channel (managed-direct wiring behind UIX.RenderApi's SpRemote*) --");
HRESULT vc = EngineService.RemoteCreateServerStreams("t", TransportProtocol.VC, out _, out _);
Check(vc.IsError(), "RemoteCreateServerStreams rejects the unrecovered VC transport (E_NOTIMPL)");
HRESULT made = EngineService.RemoteCreateServerStreams("uixrender-test", TransportProtocol.TCP, out IntPtr sendStream, out IntPtr recvStream);
Check(made.IsSuccess() && sendStream != IntPtr.Zero && recvStream != IntPtr.Zero, "RemoteCreateServerStreams (TCP) yields two stream handles");
Check(sendStream != recvStream, "send and receive are distinct handles, so releasing each is a separate safe op");
// Releasing both (RefCount 2 -> 0) tears the listener down; if the refcount/handle model
// were wrong this would double-free or throw, and the harness would never reach the end.
EngineService.ReleaseRemoteStream(sendStream);
EngineService.ReleaseRemoteStream(recvStream);
Check(true, "releasing both stream handles disposes the connection exactly once, without error");
Console.WriteLine();
Console.WriteLine(failures == 0 ? "ALL CHECKS PASSED" : $"{failures} CHECK(S) FAILED");
return failures == 0 ? 0 : 1;
// Probe types for the schema checks above -- deliberately trivial, since what's being
// verified is the projection machinery, not any particular type's shape.
internal sealed class SchemaProbe
{
public int Value { get; set; }
public int Add(int a, int b) => a + b;
}
internal enum SchemaProbeKind
{
First = 0,
Second = 5,
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\UIXrender\UIXrender.csproj" />
</ItemGroup>
</Project>
@@ -10,6 +10,10 @@ using System.Runtime.InteropServices;
using System.Security;
using Microsoft.Iris.Render.Common;
using Microsoft.Iris.Render.Internal;
#if !NETFRAMEWORK
using Microsoft.Iris.Render.Engine;
using Iface = Microsoft.Iris.Render.Interop;
#endif
namespace Microsoft.Iris.Render.Protocol
{
@@ -18,6 +22,16 @@ namespace Microsoft.Iris.Render.Protocol
{
private const string s_stEhRenderDll = "UIXRender.dll";
// SpInit/SpUninit/SpBufferOpen/SpWrapBufferProc/SpRenderThreadInit/
// SpRenderThreadUninit below call directly into UIXrender's managed
// Microsoft.Iris.Render.Engine.EngineService instead of P/Invoking into
// UIXRender.dll -- no native marshaling when both assemblies are loaded in the
// same process. Everything else in this file is untouched DllImport, since
// UIXrender doesn't implement those exports yet. See
// logs/UIXrender/EngineCore.md for the reasoning (first modification of
// previously-decompiled code in this project) and why these 6 are exactly the
// set RenderPort.cs/LocalChannel.cs actually use.
public static void IFC(HRESULT hr)
{
if (hr.Int >= 0)
@@ -102,6 +116,49 @@ namespace Microsoft.Iris.Render.Protocol
}
}
#if !NETFRAMEWORK
public static HRESULT SpInit(ref InitArgs args) => new HRESULT(0);
public static HRESULT SpUninit() => new HRESULT(0);
public static unsafe HRESULT SpBufferOpen(
BufferInfo* phdrData,
void* pvData)
{
var src = new Iface.ContextID(ContextID.ToUInt32(phdrData->idContextSrc));
var dest = new Iface.ContextID(ContextID.ToUInt32(phdrData->idContextDest));
var bufferHandle = new Iface.RENDERHANDLE(RENDERHANDLE.ToUInt32(phdrData->idBuffer));
var flags = (Iface.BufferFlags)phdrData->nFlags;
var span = new ReadOnlySpan<byte>(pvData, (int)phdrData->cbSizeBuffer);
Iface.HRESULT result = EngineService.SendBuffer(src, dest, bufferHandle, flags, span);
return new HRESULT(result.hr);
}
public static unsafe HRESULT SpWrapBufferProc(
MessageBufferEventHandler pfnProcessBufferProc,
IntPtr* ppNativeProc)
{
if (ppNativeProc == null)
return new HRESULT(unchecked((int)0x80070057));
if (pfnProcessBufferProc == null)
{
*ppNativeProc = IntPtr.Zero;
return new HRESULT(0);
}
// Managed-direct: store the delegate itself instead of marshaling to a
// native function pointer -- SpRenderThreadInit resolves this straight back
// to the delegate object and invokes it directly, no calli anywhere on this
// path. See logs/UIXrender/EngineCore.md.
GCHandle handle = GCHandle.Alloc(pfnProcessBufferProc, GCHandleType.Normal);
*ppNativeProc = GCHandle.ToIntPtr(handle);
return new HRESULT(0);
}
#else
// net461 (EnableNetFXTarget): UIXrender's managed API isn't referenceable from
// .NET Framework, so this TFM keeps calling the real native UIXRender.dll.
[DllImport(s_stEhRenderDll)]
public static extern HRESULT SpInit(ref InitArgs args);
@@ -117,7 +174,41 @@ namespace Microsoft.Iris.Render.Protocol
public static extern unsafe HRESULT SpWrapBufferProc(
MessageBufferEventHandler pfnProcessBufferProc,
IntPtr* ppNativeProc);
#endif
#if !NETFRAMEWORK
// No Win32 message queue exists behind this reimplementation (see EngineService),
// and LocalChannel -- the path Zune uses -- never peeks. Reports "no message".
public static HRESULT SpPeekMessage(
out Win32Api.MSG msg,
HWND hwnd,
uint nMsgFilterMin,
uint nMsgFilterMax,
uint wRemoveMsg,
out WorkResult nResult)
{
msg = default;
nResult = (WorkResult)EngineService.PeekMessage(nMsgFilterMin, nMsgFilterMax, wRemoveMsg);
return new HRESULT(0);
}
public static HRESULT SpWaitMessage(uint nTimeOutMs, IntPtr _unused)
{
EngineService.WaitMessage(nTimeOutMs);
return new HRESULT(0);
}
public static HRESULT SpInvoke(
ContextID idContext,
IntPtr pfnInvoke,
IntPtr pvArgs,
bool synchronous)
{
Iface.HRESULT result = EngineService.Invoke(
new Iface.ContextID(ContextID.ToUInt32(idContext)), pfnInvoke, pvArgs, synchronous);
return new HRESULT(result.hr);
}
#else
[DllImport(s_stEhRenderDll, CharSet = CharSet.Auto)]
public static extern HRESULT SpPeekMessage(
out Win32Api.MSG msg,
@@ -136,7 +227,65 @@ namespace Microsoft.Iris.Render.Protocol
IntPtr pfnInvoke,
IntPtr pvArgs,
bool synchronous);
#endif
#if !NETFRAMEWORK
public static HRESULT SpRenderThreadInit(
ref InitArgs argsRender,
out IntPtr pThread)
{
var contextId = new Iface.ContextID(ContextID.ToUInt32(argsRender.idContext));
MessageBufferEventHandler managedCallback = argsRender.pfnProcessBuffer != IntPtr.Zero
? GCHandle.FromIntPtr(argsRender.pfnProcessBuffer).Target as MessageBufferEventHandler
: null;
BufferReceivedHandler handler = managedCallback != null
? AdaptCallback(managedCallback, argsRender.idContext)
: delegate { };
IRenderThreadHandle threadHandle = EngineService.StartRenderThread(contextId, handler);
pThread = GCHandle.ToIntPtr(GCHandle.Alloc(threadHandle, GCHandleType.Normal));
return new HRESULT(0);
}
// Adapts a stored MessageBufferEventHandler (already-decompiled, still
// pointer-shaped -- see logs/UIXrender/EngineCore.md) into the idiomatic
// BufferReceivedHandler shape EngineService deals in. Factored out of
// SpRenderThreadInit so that method reads as resolve -> adapt -> start -> wrap,
// and named to make clear it's the same kind of adaptation
// UIXrender/Interop/EngineApi.cs's own SpRenderThreadInit does for native
// callers (there: raw function pointer -> BufferReceivedHandler; here: managed
// delegate -> BufferReceivedHandler -- same shape, different invocation
// mechanism at the end).
private static unsafe BufferReceivedHandler AdaptCallback(MessageBufferEventHandler callback, ContextID destContext) =>
(source, bufferHandle, flags, data) =>
{
fixed (byte* pData = data)
{
var info = new BufferInfo
{
idContextSrc = ContextID.FromUInt32(source.value),
idContextDest = destContext,
idBuffer = RENDERHANDLE.FromUInt32(bufferHandle.value),
nFlags = (BufferFlags)flags,
cbSizeBuffer = (uint)data.Length,
};
callback(IntPtr.Zero, source.value, &info, pData);
}
};
public static HRESULT SpRenderThreadUninit(IntPtr pThread)
{
if (pThread == IntPtr.Zero)
return new HRESULT(unchecked((int)0x80070057));
GCHandle handle = GCHandle.FromIntPtr(pThread);
(handle.Target as IDisposable)?.Dispose();
handle.Free();
return new HRESULT(0);
}
#else
[DllImport(s_stEhRenderDll)]
public static extern HRESULT SpRenderThreadInit(
ref InitArgs argsRender,
@@ -144,7 +293,86 @@ namespace Microsoft.Iris.Render.Protocol
[DllImport(s_stEhRenderDll)]
public static extern HRESULT SpRenderThreadUninit(IntPtr pThread);
#endif
#if !NETFRAMEWORK
public static HRESULT SpRemoteCreateServerStreams(
string stSession,
TransportProtocol nProtocol,
out IntPtr pSendStream,
out IntPtr pReceiveStream)
{
Iface.HRESULT result = EngineService.RemoteCreateServerStreams(
stSession, (Iface.Protocol.TransportProtocol)(int)nProtocol, out pSendStream, out pReceiveStream);
return new HRESULT(result.hr);
}
public static HRESULT SpRemoteWaitServerStreamsConnected(
TransportProtocol nProtocol,
IntPtr pSendStream,
IntPtr pReceiveStream)
{
Iface.HRESULT result = EngineService.RemoteWaitServerStreamsConnected(
(Iface.Protocol.TransportProtocol)(int)nProtocol, pSendStream);
return new HRESULT(result.hr);
}
public static HRESULT SpRemoteServerInit(
IntPtr pSendStream,
IntPtr pReceiveStream,
InitArgs argsSend,
out IntPtr pSession)
{
var context = new Iface.ContextID(ContextID.ToUInt32(argsSend.idContext));
// Same delegate-behind-a-GCHandle representation SpRenderThreadInit resolves;
// RemoteChannel connects without a receive callback (pfnProcessBuffer == 0),
// so this is normally null.
MessageBufferEventHandler managedCallback = argsSend.pfnProcessBuffer != IntPtr.Zero
? GCHandle.FromIntPtr(argsSend.pfnProcessBuffer).Target as MessageBufferEventHandler
: null;
BufferReceivedHandler handler = managedCallback != null
? AdaptCallback(managedCallback, argsSend.idContext)
: null;
Iface.HRESULT result = EngineService.RemoteServerInit(pSendStream, context, handler, out pSession);
return new HRESULT(result.hr);
}
public static HRESULT SpRemoteServerUninit(
IntPtr pSession,
bool fForceShutdown,
out ShutdownReason nShutdownReason)
{
Iface.HRESULT result = EngineService.RemoteServerUninit(
pSession, fForceShutdown, out Iface.Protocol.ShutdownReason reason);
nShutdownReason = (ShutdownReason)(int)reason;
return new HRESULT(result.hr);
}
public static HRESULT SpDx9CompileEffect(
string stEffect,
string stDefines,
out IntPtr pErrorString,
out IntPtr pErrorBuffer,
out IntPtr pEffectBlob,
out uint EffectBlobSize,
out IntPtr pEffectBlobBuffer)
{
pErrorString = IntPtr.Zero;
pErrorBuffer = IntPtr.Zero;
pEffectBlob = IntPtr.Zero;
EffectBlobSize = 0U;
pEffectBlobBuffer = IntPtr.Zero;
return new HRESULT(EngineService.Dx9CompileEffect().hr);
}
// SpObjectRelease's only callers (RemoteChannel) release the stream handles from
// SpRemoteCreateServerStreams, which in the managed-direct path are UIXrender
// handles, not COM pointers -- so this drops the handle's reference rather than
// calling through a vtable.
public static void SpObjectRelease(IntPtr pUnknown) => EngineService.ReleaseRemoteStream(pUnknown);
#else
[DllImport(s_stEhRenderDll, CharSet = CharSet.Unicode)]
public static extern HRESULT SpRemoteCreateServerStreams(
string stSession,
@@ -183,6 +411,7 @@ namespace Microsoft.Iris.Render.Protocol
[DllImport(s_stEhRenderDll)]
public static extern void SpObjectRelease(IntPtr pUnknown);
#endif
[Flags]
public enum BufferFlags
@@ -581,7 +581,7 @@ namespace Microsoft.Iris.Render.Protocol
switch (operation)
{
case ObjectCache.Operation.Alloc:
key = new MessageHeap(8U * Win32Api.GetSystemPageSize(), (uint)sizeof(MessageBatchHeader), (uint)sizeof(MessageBatchEntry));
key = new MessageHeap(8U * (uint)Environment.SystemPageSize, (uint)sizeof(MessageBatchHeader), (uint)sizeof(MessageBatchEntry));
this._activeHeaps[key] = key;
break;
case ObjectCache.Operation.Free:
+6
View File
@@ -10,4 +10,10 @@
<EnableNetFXTarget>true</EnableNetFXTarget>
</PropertyGroup>
<ItemGroup Condition=" '$(TargetFramework)' != 'net461' ">
<!-- EngineApi.cs calls directly into UIXrender's managed API on this TFM
instead of P/Invoking UIXRender.dll; not referenceable from net461. -->
<ProjectReference Include="..\UIXrender\UIXrender.csproj" />
</ItemGroup>
</Project>
+23
View File
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
namespace Microsoft.Iris.Interop;
@@ -11,4 +12,26 @@ public static unsafe class NativeString
public static string AnsiToString(byte* p) => p == null ? null : Marshal.PtrToStringAnsi((IntPtr)p);
public static string UniToString(char* p) => p == null ? null : Marshal.PtrToStringUni((IntPtr)p);
// Interned, caller-does-not-free native copies. Several exports hand a `char*` back
// out (SpQueryTypeName, SpQueryPropertyName, SpQueryMethodName, SpQueryEnumName, ...)
// and their managed declarations never free what they receive -- so the callee has to
// own the memory for the process lifetime. Interning by value keeps that bounded:
// these are schema names drawn from a fixed set of registered types, so the pool
// stops growing once every name has been asked for once, instead of leaking a fresh
// allocation per call.
private static readonly ConcurrentDictionary<string, IntPtr> s_interned = new(StringComparer.Ordinal);
public static char* InternUni(string value)
{
if (value == null)
return null;
return (char*)s_interned.GetOrAdd(value, static v => Marshal.StringToHGlobalUni(v));
}
// Non-interned copy, for values that are genuinely per-call (query results, converted
// strings) rather than fixed schema names. The caller frees these via SpMemFree.
public static char* AllocUni(string value) =>
value == null ? null : (char*)Marshal.StringToHGlobalUni(value);
}
@@ -0,0 +1,9 @@
using System;
using Microsoft.Iris.Render.Interop;
namespace Microsoft.Iris.Render.Engine;
// A custom (non-generic) delegate so ReadOnlySpan<byte> is legal here -- Span<T> can't
// be an Action<T>/Func<T> 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<byte> data);
+9 -23
View File
@@ -1,35 +1,21 @@
using System;
using System.Collections.Concurrent;
using Microsoft.Iris.Render.Interop;
namespace Microsoft.Iris.Render.Engine;
// Maps a registered ContextID to the buffer-processing callback it should receive
// deliveries on, via SpBufferOpen -- the smallest coherent slice of "Engine core" (see
// logs/UIXrender/EngineCore.md) needed for SpRenderThreadInit and SpBufferOpen to mean
// anything together. Callback stored as a raw IntPtr (not a typed function pointer
// field) so nothing here needs to be unsafe/deal with function-pointer-as-generic-arg
// concerns; callers cast at the point they actually invoke it.
// 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
{
internal readonly struct Entry
{
public readonly IntPtr Callback;
public readonly IntPtr CallbackData;
private static readonly ConcurrentDictionary<uint, BufferReceivedHandler> s_contexts = new();
public Entry(IntPtr callback, IntPtr callbackData)
{
Callback = callback;
CallbackData = callbackData;
}
}
private static readonly ConcurrentDictionary<uint, Entry> s_contexts = new();
public static void Register(ContextID id, IntPtr callback, IntPtr callbackData)
=> s_contexts[id.value] = new Entry(callback, callbackData);
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 Entry entry) => s_contexts.TryGetValue(id.value, out entry);
public static bool TryGet(ContextID id, out BufferReceivedHandler handler) => s_contexts.TryGetValue(id.value, out handler);
}
+95
View File
@@ -0,0 +1,95 @@
using System;
using Microsoft.Iris.Render.Interop;
using Microsoft.Iris.Render.Interop.Protocol;
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<byte> data)
{
if (!ContextRegistry.TryGet(destContext, out BufferReceivedHandler handler))
return HRESULT.E_FAIL;
handler(sourceContext, bufferHandle, flags, data);
return HRESULT.S_OK;
}
// ---- message pump ----------------------------------------------------------------
// There is no Win32 message queue behind this reimplementation yet (no window is
// created anywhere), and LocalChannel -- the path real Zune uses -- never peeks: it
// relies on the SendBuffer callback instead. So a peek always reports "no message".
// Returns the WorkResult flags value (0 = nothing processed / no new message).
public static uint PeekMessage(uint filterMin, uint filterMax, uint removeMsg) => 0;
// Real timeout wait (there is no message to wake early for, per PeekMessage's note),
// so "wait for a message or the timeout" collapses to waiting out the timeout.
public static void WaitMessage(uint timeoutMs)
{
if (timeoutMs != 0)
System.Threading.Thread.Sleep((int)Math.Min(timeoutMs, int.MaxValue));
}
// Runs a deferred callback. The only in-repo caller (IRenderEngine.InterThreadWake)
// passes a null function pointer purely to wake a pump -- and there is no blocking
// pump here -- so a null pointer is a well-defined no-op rather than an error.
public static unsafe HRESULT Invoke(ContextID context, IntPtr pfnInvoke, IntPtr pvArgs, bool synchronous)
{
if (pfnInvoke == IntPtr.Zero)
return HRESULT.S_OK;
if (synchronous)
{
((delegate* unmanaged<IntPtr, void>)pfnInvoke)(pvArgs);
}
else
{
IntPtr fn = pfnInvoke;
IntPtr args = pvArgs;
System.Threading.ThreadPool.QueueUserWorkItem(_ => ((delegate* unmanaged<IntPtr, void>)fn)(args));
}
return HRESULT.S_OK;
}
// ---- 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;
}
+42
View File
@@ -0,0 +1,42 @@
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<T>(IntPtr handle) where T : class =>
handle == IntPtr.Zero ? null : GCHandle.FromIntPtr(handle).Target as T;
public static bool TryGet<T>(IntPtr handle, out T value) where T : class
{
value = Get<T>(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();
}
}
}
+9
View File
@@ -0,0 +1,9 @@
using System;
using Microsoft.Iris.Render.Interop;
namespace Microsoft.Iris.Render.Engine;
public interface IRenderThreadHandle : IDisposable
{
ContextID ContextId { get; }
}
+21 -34
View File
@@ -4,59 +4,46 @@ using Microsoft.Iris.Render.Interop;
namespace Microsoft.Iris.Render.Engine;
// Backs SpRenderThreadInit/SpRenderThreadUninit: spins up a genuine OS thread the CLR
// didn't create, which invokes the registered buffer-processing callback 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() -> SpRenderThreadInit. 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.
internal sealed unsafe class RenderThread
// 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 ContextID _contextId;
private readonly IntPtr _callback;
private readonly IntPtr _callbackData;
private readonly BufferReceivedHandler _handler;
private readonly ManualResetEventSlim _shutdown = new(false);
private readonly Thread _thread;
private RenderThread(ContextID contextId, IntPtr callback, IntPtr callbackData)
public ContextID ContextId { get; }
private RenderThread(ContextID contextId, BufferReceivedHandler handler)
{
_contextId = contextId;
_callback = callback;
_callbackData = callbackData;
ContextId = contextId;
_handler = handler;
_thread = new Thread(Run) { IsBackground = true, Name = $"UIXrender.RenderThread[{contextId.value}]" };
}
public static RenderThread Start(in InitArgs args)
public static RenderThread Start(ContextID contextId, BufferReceivedHandler handler)
{
var thread = new RenderThread(args.idContext, args.pfnProcessBuffer, args.pvProcessData);
ContextRegistry.Register(args.idContext, args.pfnProcessBuffer, args.pvProcessData);
var thread = new RenderThread(contextId, handler);
ContextRegistry.Register(contextId, handler);
thread._thread.Start();
return thread;
}
private void Run()
{
if (_callback != IntPtr.Zero)
{
var info = new BufferInfo
{
idContextSrc = _contextId,
idContextDest = _contextId,
idBuffer = RENDERHANDLE.NULL,
nFlags = 0,
cbSizeBuffer = 0,
};
var fn = (delegate* unmanaged<IntPtr, uint, BufferInfo*, void*, int>)_callback;
fn(_callbackData, _contextId.value, &info, null);
}
_handler?.Invoke(ContextId, RENDERHANDLE.NULL, default, ReadOnlySpan<byte>.Empty);
_shutdown.Wait();
}
public void Stop()
public void Dispose()
{
ContextRegistry.Unregister(_contextId);
ContextRegistry.Unregister(ContextId);
_shutdown.Set();
_thread.Join();
_shutdown.Dispose();
+47
View File
@@ -0,0 +1,47 @@
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<IntPtr, uint>)fn)(comObject);
}
public static uint Release(IntPtr comObject)
{
void* fn = Slot(comObject, SlotRelease);
return fn == null ? 0 : ((delegate* unmanaged<IntPtr, uint>)fn)(comObject);
}
}
+21
View File
@@ -0,0 +1,21 @@
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));
}
+13
View File
@@ -0,0 +1,13 @@
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;
}
+11
View File
@@ -0,0 +1,11 @@
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;
}
+24
View File
@@ -0,0 +1,24 @@
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;
}
+17
View File
@@ -0,0 +1,17 @@
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;
}
}

Some files were not shown because too many files have changed in this diff Show More