mirror of
https://github.com/ZuneDev/MicrosoftIris.git
synced 2026-07-27 13:13:29 -07:00
More engine API work
This commit is contained in:
@@ -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."
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System;
|
||||
using Microsoft.Iris.Render.Interop;
|
||||
|
||||
namespace Microsoft.Iris.Render.Engine;
|
||||
|
||||
public interface IRenderThreadHandle : IDisposable
|
||||
{
|
||||
ContextID ContextId { get; }
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user