diff --git a/.github/workflows/uixrender-ci.yml b/.github/workflows/uixrender-ci.yml
new file mode 100644
index 0000000..0dc0299
--- /dev/null
+++ b/.github/workflows/uixrender-ci.yml
@@ -0,0 +1,37 @@
+name: UIXrender CI
+
+on:
+ push:
+ pull_request:
+
+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
+ # 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.
+ runs-on: windows-latest
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - uses: actions/setup-dotnet@v4
+ with:
+ dotnet-version: "8.0.x"
+
+ - name: Restore
+ run: dotnet restore MicrosoftIris.sln
+
+ - name: Build solution
+ run: dotnet build MicrosoftIris.sln -c Release --no-restore
+
+ - 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
+
+ - name: Publish UIXsup (NativeAOT, win-x64)
+ run: dotnet publish UIXsup/UIXsup.csproj -c Release -f net8.0 -r win-x64 --self-contained -p:PublishAot=true
+
+ # TODO: run the Windows-only interop test harness (Tests/UIXrender.Interop.Tests)
+ # against the published DLLs once it exists (Phase 0 spike step) — see
+ # logs/UIXrender/Architecture.md and the plan for context.
diff --git a/Directory.Build.props b/Directory.Build.props
index 18e8638..668cdda 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -19,7 +19,9 @@
- $(TargetFrameworks);net461;net8.0-windows10.0.22000
+ $(TargetFrameworks);net8.0-windows10.0.22000
+
+ $(TargetFrameworks);net461;
diff --git a/UIX.RenderApi/UIX.RenderApi.csproj b/UIX.RenderApi/UIX.RenderApi.csproj
index c2af7a6..36eadfb 100644
--- a/UIX.RenderApi/UIX.RenderApi.csproj
+++ b/UIX.RenderApi/UIX.RenderApi.csproj
@@ -7,5 +7,7 @@
false
true
+
+ true
\ No newline at end of file
diff --git a/UIX/UIX.csproj b/UIX/UIX.csproj
index 43fcc1a..8367b91 100644
--- a/UIX/UIX.csproj
+++ b/UIX/UIX.csproj
@@ -7,6 +7,8 @@
false
true
+
+ true
diff --git a/UIXrender/UIXrender.csproj b/UIXrender/UIXrender.csproj
index 8db8e17..3357bf2 100644
--- a/UIXrender/UIXrender.csproj
+++ b/UIXrender/UIXrender.csproj
@@ -6,5 +6,12 @@
Microsoft.Iris.Render
true
true
+
+ true
+
+
+ Shared
\ No newline at end of file
diff --git a/UIXsup/DebugCategory.cs b/UIXsup/DebugCategory.cs
new file mode 100644
index 0000000..8ef3be9
--- /dev/null
+++ b/UIXsup/DebugCategory.cs
@@ -0,0 +1,48 @@
+namespace Microsoft.Iris.Support;
+
+// Mirrors Microsoft.Iris.Render.DebugCategory (UIX.RenderApi/Microsoft/Iris/Render/DebugCategory.cs)
+// field-for-field: ordinal values are part of the wire contract with the already-shipped
+// managed callers (DebugGetCategoryLevel/DebugSetCategoryLevel pass this by value), so
+// the order here must match exactly, not just the names.
+public enum DebugCategory
+{
+ Animation,
+ Critical,
+ Deprecated,
+ Dispatcher,
+ Dump,
+ Dx9Wrapper,
+ DynamicSurface,
+ Failure,
+ FormWindow,
+ GraphicsCache,
+ IFC,
+ ObjectCache,
+ Performance,
+ Profile,
+ Queues,
+ Render,
+ Sounds,
+ Surface,
+ SurfacePool,
+ SurfaceRestoration,
+ Timeouts,
+ Transport,
+ WindowState,
+ Effect,
+ HTTP,
+ ExternalCount,
+ Protocol,
+ RenderHost,
+ DumpException,
+ Focus,
+ Input,
+ TreeDisposal,
+ VisualSizePos,
+ Visual,
+ Painting,
+ RenderWindow,
+ ImageLoad,
+ ImageCache,
+ TotalCount,
+}
diff --git a/UIXsup/DebugState.cs b/UIXsup/DebugState.cs
new file mode 100644
index 0000000..4199d6a
--- /dev/null
+++ b/UIXsup/DebugState.cs
@@ -0,0 +1,22 @@
+namespace Microsoft.Iris.Support;
+
+internal static class DebugState
+{
+ private static readonly byte[] s_categoryLevels = new byte[(int)DebugCategory.TotalCount];
+
+ public static bool TimedWriteLines { get; set; }
+ public static string WriteLinePrefix { get; set; } = string.Empty;
+
+ public static byte GetCategoryLevel(DebugCategory category)
+ {
+ int index = (int)category;
+ return (uint)index < (uint)s_categoryLevels.Length ? s_categoryLevels[index] : (byte)0;
+ }
+
+ public static void SetCategoryLevel(DebugCategory category, byte level)
+ {
+ int index = (int)category;
+ if ((uint)index < (uint)s_categoryLevels.Length)
+ s_categoryLevels[index] = level;
+ }
+}
diff --git a/UIXsup/Interop/DebugApi.cs b/UIXsup/Interop/DebugApi.cs
new file mode 100644
index 0000000..2c14ebb
--- /dev/null
+++ b/UIXsup/Interop/DebugApi.cs
@@ -0,0 +1,38 @@
+using System;
+using System.Runtime.InteropServices;
+
+namespace Microsoft.Iris.Support.Interop;
+
+// [UnmanagedCallersOnly] exports matching the 5 DllImport("UIXsup.dll") declarations in
+// the already-shipped managed callers (eDebugApi.cs / DebugHelpers.cs) field-for-field.
+// See logs/UIXrender/UIXsup.md for why parameters are byte*/int here instead of
+// string/bool (ANSI marshaling, blittability) and for the DebugDisplayErrorStack scope
+// gap noted below.
+public static unsafe class DebugApi
+{
+ [UnmanagedCallersOnly(EntryPoint = "DebugDisplayErrorStack")]
+ public static int DebugDisplayErrorStack(byte* stMessage, byte* filename, int line, byte* title, byte* stackTrace)
+ {
+ string timestamp = DebugState.TimedWriteLines ? $"[{DateTime.Now:HH:mm:ss.fff}] " : string.Empty;
+ Console.Error.WriteLine(
+ $"{timestamp}{DebugState.WriteLinePrefix}{PtrToString(title)}: {PtrToString(stMessage)} ({PtrToString(filename)}:{line})\n{PtrToString(stackTrace)}");
+
+ // TODO: no interactive dialog UI yet (see logs/UIXrender/UIXsup.md) -- always
+ // reports "don't break".
+ return 0;
+ }
+
+ [UnmanagedCallersOnly(EntryPoint = "DebugSetTimedWriteLines")]
+ public static void DebugSetTimedWriteLines(int fEnabled) => DebugState.TimedWriteLines = fEnabled != 0;
+
+ [UnmanagedCallersOnly(EntryPoint = "DebugSetWriteLinePrefix")]
+ public static void DebugSetWriteLinePrefix(byte* stPrefix) => DebugState.WriteLinePrefix = PtrToString(stPrefix) ?? string.Empty;
+
+ [UnmanagedCallersOnly(EntryPoint = "DebugGetCategoryLevel")]
+ public static byte DebugGetCategoryLevel(DebugCategory cat) => DebugState.GetCategoryLevel(cat);
+
+ [UnmanagedCallersOnly(EntryPoint = "DebugSetCategoryLevel")]
+ public static void DebugSetCategoryLevel(DebugCategory cat, byte level) => DebugState.SetCategoryLevel(cat, level);
+
+ private static string PtrToString(byte* p) => p == null ? null : Marshal.PtrToStringAnsi((IntPtr)p);
+}
diff --git a/UIXsup/UIXsup.csproj b/UIXsup/UIXsup.csproj
index 42f5411..ac9951d 100644
--- a/UIXsup/UIXsup.csproj
+++ b/UIXsup/UIXsup.csproj
@@ -6,5 +6,12 @@
Microsoft.Iris.Support
true
true
+
+ true
+
+
+ Shared
\ No newline at end of file