diff --git a/MicrosoftIris.sln b/MicrosoftIris.sln
index 24f24d5..f105956 100644
--- a/MicrosoftIris.sln
+++ b/MicrosoftIris.sln
@@ -28,6 +28,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXrender.Interop.Tests", "
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIXrender.Engine.Tests", "Tests\UIXrender.Engine.Tests\UIXrender.Engine.Tests.csproj", "{568E8DCD-CAB3-499B-A264-C62B1E1082B7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UIX.RenderApi.OpenGL", "UIX.RenderApi.OpenGL\UIX.RenderApi.OpenGL.csproj", "{6FE88A00-7281-4571-BBA3-83AAD8690049}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -146,6 +148,18 @@ Global
{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
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|x64.ActiveCfg = Debug|x64
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|x64.Build.0 = Debug|x64
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|x86.ActiveCfg = Debug|x86
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Debug|x86.Build.0 = Debug|x86
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|Any CPU.Build.0 = Release|Any CPU
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|x64.ActiveCfg = Release|x64
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|x64.Build.0 = Release|x64
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|x86.ActiveCfg = Release|x86
+ {6FE88A00-7281-4571-BBA3-83AAD8690049}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/UIX.RenderApi.OpenGL/Animation/GLAnimation.cs b/UIX.RenderApi.OpenGL/Animation/GLAnimation.cs
new file mode 100644
index 0000000..8fa90e7
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Animation/GLAnimation.cs
@@ -0,0 +1,67 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Shared state machine for animations: play/pause/reset, repeat counting and the
+ /// async-notify event. Target property interpolation is intentionally minimal here
+ /// (see ); full per-frame evaluation is a stage-3 TODO.
+ ///
+ public abstract class GLAnimation : SharedRenderObject, IAnimation
+ {
+ public int RepeatCount { get; set; }
+ public bool IsPlaying { get; private set; }
+ public bool IsActive { get; private set; }
+ public bool AutoReset { get; set; }
+ public AnimationResetBehavior ResetBehavior { get; set; } = AnimationResetBehavior.LeaveCurrent;
+
+ public event AsyncNotifyHandler? AsyncNotifyEvent;
+
+ public virtual void Play()
+ {
+ IsPlaying = true;
+ IsActive = true;
+ }
+
+ public virtual void Pause() => IsPlaying = false;
+
+ public virtual void Reset()
+ {
+ IsPlaying = false;
+ IsActive = false;
+ }
+
+ public virtual void InstantAdvance(float advanceTime) { }
+
+ public virtual void InstantFinish()
+ {
+ IsPlaying = false;
+ IsActive = false;
+ }
+
+ protected void RaiseAsyncNotify(int cookie) => AsyncNotifyEvent?.Invoke(cookie);
+
+ /// Advance internal time. Called by the animation system each pulse.
+ internal virtual void Advance(int advanceMs) { }
+ }
+
+ public sealed class GLAnimationGroup : GLAnimation, IAnimationGroup
+ {
+ private readonly List m_members = new List();
+
+ public override void Play()
+ {
+ base.Play();
+ foreach (GLAnimation a in m_members)
+ a.Play();
+ }
+
+ internal void Add(GLAnimation animation) => m_members.Add(animation);
+
+ internal override void Advance(int advanceMs)
+ {
+ foreach (GLAnimation a in m_members)
+ a.Advance(advanceMs);
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Animation/GLAnimationSystem.cs b/UIX.RenderApi.OpenGL/Animation/GLAnimationSystem.cs
new file mode 100644
index 0000000..b48806d
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Animation/GLAnimationSystem.cs
@@ -0,0 +1,60 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Owns the set of live animations and advances them on each pulse. Pause/step/resume
+ /// gate whether time flows. Objects are created here so the session stays a thin factory.
+ ///
+ public sealed class GLAnimationSystem : IAnimationSystem
+ {
+ private readonly List m_animations = new List();
+ private bool m_paused;
+
+ public int UpdatesPerSecond { get; set; } = 60;
+ public float SpeedAdjustment { get; set; } = 1f;
+ public bool BackCompat { set { /* compatibility flag; no behavioral change */ } }
+
+ public IKeyframeAnimation CreateKeyframeAnimation(object objUser, AnimationInput initialValue)
+ {
+ var a = new GLKeyframeAnimation(initialValue);
+ m_animations.Add(a);
+ return a;
+ }
+
+ public IAnimationGroup CreateAnimationGroup(object objUser)
+ {
+ var g = new GLAnimationGroup();
+ m_animations.Add(g);
+ return g;
+ }
+
+ public IExternalAnimationInput CreateExternalAnimationInput(object objUser, IAnimationPropertyMap propertyMap)
+ => new GLExternalAnimationInput(propertyMap);
+
+ public void PulseTimeAdvance(int nAdvanceMs)
+ {
+ if (m_paused)
+ return;
+ int scaled = (int)(nAdvanceMs * SpeedAdjustment);
+ foreach (GLAnimation a in m_animations)
+ {
+ if (a.IsPlaying)
+ a.Advance(scaled);
+ }
+ }
+
+ public void PauseAnimations() => m_paused = true;
+
+ public void StepAnimations(int nAdvanceMs)
+ {
+ foreach (GLAnimation a in m_animations)
+ {
+ if (a.IsPlaying)
+ a.Advance(nAdvanceMs);
+ }
+ }
+
+ public void ResumeAnimations() => m_paused = false;
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Animation/GLExternalAnimationInput.cs b/UIX.RenderApi.OpenGL/Animation/GLExternalAnimationInput.cs
new file mode 100644
index 0000000..8be489a
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Animation/GLExternalAnimationInput.cs
@@ -0,0 +1,42 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// An externally-driven animation input. Providers publish named values that
+ /// animations can reference. We store the published values; wiring them into
+ /// keyframe evaluation is a stage-3 TODO alongside full animation support.
+ ///
+ public sealed class GLExternalAnimationInput : SharedRenderObject, IExternalAnimationInput
+ {
+ private static uint s_nextId = 1;
+
+ public GLExternalAnimationInput(IAnimationPropertyMap? propertyMap)
+ {
+ UniqueId = s_nextId++;
+ PropertyMap = propertyMap;
+ }
+
+ public uint UniqueId { get; }
+ internal IAnimationPropertyMap? PropertyMap { get; }
+
+ public IAnimationInputProvider CreateProvider(object objUser) => new GLAnimationInputProvider();
+ }
+
+ public sealed class GLAnimationInputProvider : SharedRenderObject, IAnimationInputProvider
+ {
+ private readonly Dictionary m_values = new Dictionary();
+
+ public void PublishFloat(string propertyName, float value) => m_values[propertyName] = value;
+ public void PublishVector2(string propertyName, Vector2 value) => m_values[propertyName] = value;
+ public void PublishVector3(string propertyName, Vector3 value) => m_values[propertyName] = value;
+ public void PublishVector4(string propertyName, Vector4 value) => m_values[propertyName] = value;
+ public void PublishQuaternion(string propertyName, Quaternion value) => m_values[propertyName] = value;
+
+ public void RevokeFloat(string propertyName) => m_values.Remove(propertyName);
+ public void RevokeVector2(string propertyName) => m_values.Remove(propertyName);
+ public void RevokeVector3(string propertyName) => m_values.Remove(propertyName);
+ public void RevokeVector4(string propertyName) => m_values.Remove(propertyName);
+ public void RevokeQuaternion(string propertyName) => m_values.Remove(propertyName);
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Animation/GLKeyframeAnimation.cs b/UIX.RenderApi.OpenGL/Animation/GLKeyframeAnimation.cs
new file mode 100644
index 0000000..2bcc876
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Animation/GLKeyframeAnimation.cs
@@ -0,0 +1,66 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Keyframe animation. Stores keyframes, targets and events and runs the play-state
+ /// machine. Smooth per-frame evaluation and target property mutation are a stage-3
+ /// TODO; today it drives lifecycle/events so higher layers sequence correctly.
+ ///
+ public sealed class GLKeyframeAnimation : GLAnimation, IKeyframeAnimation
+ {
+ private readonly struct Target
+ {
+ public readonly IAnimatable Object;
+ public readonly string Property;
+ public readonly string? Mask;
+ public Target(IAnimatable o, string property, string? mask)
+ {
+ Object = o;
+ Property = property;
+ Mask = mask;
+ }
+ }
+
+ private readonly List m_keyframes = new List();
+ private readonly List m_targets = new List();
+ private readonly List m_events = new List();
+
+ public GLKeyframeAnimation(AnimationInput initialValue)
+ {
+ InitialValue = initialValue;
+ Type = initialValue.InputType;
+ }
+
+ public int KeyframeCount => m_keyframes.Count;
+ public AnimationInput InitialValue { get; }
+ public AnimationInput Reference { get; set; } = null!;
+ public AnimationInput Scale { get; set; } = null!;
+ public AnimationInputType Type { get; }
+
+ public void AddKeyframe(AnimationKeyframe keyframe) => m_keyframes.Add(keyframe);
+
+ public AnimationKeyframe GetKeyframe(int keyframeIndex) => m_keyframes[keyframeIndex];
+
+ public void SetKeyframe(int keyframeIndex, AnimationKeyframe keyframe) => m_keyframes[keyframeIndex] = keyframe;
+
+ public void AddTarget(IAnimatable targetObject, string targetProperty)
+ => m_targets.Add(new Target(targetObject, targetProperty, null));
+
+ public void AddTarget(IAnimatable targetObject, string targetProperty, string targetPropertyMask)
+ => m_targets.Add(new Target(targetObject, targetProperty, targetPropertyMask));
+
+ public void RemoveTarget(IAnimatable targetObject, string targetProperty, string targetPropertyMask)
+ => m_targets.RemoveAll(t => ReferenceEquals(t.Object, targetObject)
+ && t.Property == targetProperty && t.Mask == targetPropertyMask);
+
+ public void RemoveAllTargets() => m_targets.Clear();
+
+ public void AddStageEvent(AnimationStage animationStage, AnimationEvent animationEvent) => m_events.Add(animationEvent);
+ public void AddTimeEvent(float absoluteTime, AnimationEvent animationEvent) => m_events.Add(animationEvent);
+ public void AddProgressEvent(float progress, AnimationEvent animationEvent) => m_events.Add(animationEvent);
+ public void AddValueEvent(ValueEventCondition condition, AnimationInput reference, AnimationEvent animationEvent) => m_events.Add(animationEvent);
+ public void RemoveEvent(AnimationEvent animationEvent) => m_events.Remove(animationEvent);
+ public void RemoveAllEvents() => m_events.Clear();
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLDisplay.cs b/UIX.RenderApi.OpenGL/Engine/GLDisplay.cs
new file mode 100644
index 0000000..9620ab4
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLDisplay.cs
@@ -0,0 +1,65 @@
+using Silk.NET.Maths;
+using Silk.NET.Windowing;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// A monitor, described from a Silk.NET . Resolution changes are
+ /// reported as unsupported (the in-process renderer runs windowed).
+ ///
+ public sealed class GLDisplay : IDisplay
+ {
+ private readonly DisplayMode m_mode;
+
+ public GLDisplay(IMonitor monitor, bool isPrimary)
+ {
+ DeviceName = monitor.Name ?? $"Monitor{monitor.Index}";
+ Rectangle b = monitor.Bounds;
+ ScreenArea = new Rectangle(b.Origin.X, b.Origin.Y, b.Size.X, b.Size.Y);
+ WorkArea = ScreenArea;
+ IsPrimary = isPrimary;
+
+ var size = new Size(b.Size.X, b.Size.Y);
+ LogicalFullScreenResolution = size;
+ m_mode = new DisplayMode
+ {
+ sizePhysicalPxl = size,
+ sizeLogicalPxl = size,
+ nRefreshRate = monitor.VideoMode.RefreshRate ?? 60,
+ fInterlaced = false,
+ fTvMode = false,
+ };
+ }
+
+ public string DeviceName { get; }
+ public Rectangle ScreenArea { get; }
+ public Rectangle WorkArea { get; }
+ public bool IsPrimary { get; }
+ public TvFormat TvFormat => TvFormat.None;
+ public bool TvMode => false;
+ public Size LogicalFullScreenResolution { get; }
+
+ public DisplayMode[] SupportedModes => new[] { m_mode };
+ public DisplayMode[] ExtraModes => DisplayMode.EmptyModes;
+ public DisplayMode[] AllModes => SupportedModes;
+ public DisplayMode CurrentMode => m_mode;
+ public DisplayMode DesktopMode => m_mode;
+ public string MonitorPnP => DeviceName;
+
+ public bool ValidateDisplayMode(
+ DisplayMode modeDesired,
+ DisplayModeFlags nCheck,
+ bool fAllowAllModes,
+ out DisplayMode modeComplete,
+ out DisplayModeFlags nCompleteCheck)
+ {
+ // We only expose the desktop mode, so echo it back as the completed mode.
+ modeComplete = m_mode;
+ nCompleteCheck = nCheck;
+ return true;
+ }
+
+ // Windowed in-process rendering cannot switch the monitor's resolution.
+ public bool ChangeFullScreenResolution(DisplayMode modeChanges, DisplayModeFlags nValid) => false;
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLDisplayManager.cs b/UIX.RenderApi.OpenGL/Engine/GLDisplayManager.cs
new file mode 100644
index 0000000..a426518
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLDisplayManager.cs
@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Silk.NET.Windowing;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Enumerates monitors via Silk.NET and exposes them as s.
+ ///
+ public sealed class GLDisplayManager : IDisplayManager
+ {
+ private readonly List m_displays = new List();
+
+ public GLDisplayManager(IView view)
+ {
+ IMonitor? primary = Monitor.GetMainMonitor(view);
+ foreach (IMonitor monitor in Monitor.GetMonitors(view))
+ {
+ bool isPrimary = primary != null && monitor.Index == primary.Index;
+ var display = new GLDisplay(monitor, isPrimary);
+ if (isPrimary)
+ m_displays.Insert(0, display);
+ else
+ m_displays.Add(display);
+ }
+
+ if (m_displays.Count == 0 && primary != null)
+ m_displays.Add(new GLDisplay(primary, true));
+ }
+
+ public IDisplay PrimaryDisplay => m_displays[0];
+
+ public DisplayMode[] ExtraModes { get; set; } = DisplayMode.EmptyModes;
+
+ public IDisplay DisplayFromDeviceName(string stDeviceName)
+ {
+ foreach (GLDisplay d in m_displays)
+ {
+ if (d.DeviceName == stDeviceName)
+ return d;
+ }
+ return PrimaryDisplay;
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLGraphicsDevice.cs b/UIX.RenderApi.OpenGL/Engine/GLGraphicsDevice.cs
new file mode 100644
index 0000000..835c2c1
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLGraphicsDevice.cs
@@ -0,0 +1,53 @@
+using System;
+using Silk.NET.OpenGL;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Graphics device wrapping the live OpenGL context.
+ ///
+ public sealed class GLGraphicsDevice : IGraphicsDevice
+ {
+ private readonly GL m_gl;
+ private readonly Action m_renderNow;
+ private string? m_captureFileName;
+
+ public GLGraphicsDevice(GL gl, GraphicsRenderingQuality quality, Action renderNow)
+ {
+ m_gl = gl;
+ m_renderNow = renderNow;
+ RenderingQuality = quality;
+
+ m_gl.GetInteger(GetPName.MaxTextureSize, out int maxTex);
+ if (maxTex <= 0)
+ maxTex = 2048;
+ MaximumImageSize = new Size(maxTex, maxTex);
+ }
+
+ // The RenderApi enum has no OpenGL member; we present as the hardware-accelerated
+ // path (Direct3D9) since the UI branches on GDI-vs-accelerated, not the exact API.
+ // TODO(stage 3): extend GraphicsDeviceType if a distinct OpenGL identity is needed.
+ public GraphicsDeviceType DeviceType => GraphicsDeviceType.Direct3D9;
+
+ public Size MaximumImageSize { get; }
+ public bool IsVideoComposited => false;
+ public GraphicsRenderingQuality RenderingQuality { get; }
+
+ public event BackBufferCapturedHandler? BackBufferCapturedEvent;
+
+ public void RenderNowIfPossible() => m_renderNow();
+
+ public void BeginCaptureBackBuffer(string stFileName) => m_captureFileName = stFileName;
+
+ public void EndCaptureBackBuffer()
+ {
+ // TODO(stage 3): read back the framebuffer to m_captureFileName. For now we
+ // only signal completion so callers waiting on the event proceed.
+ m_captureFileName = null;
+ BackBufferCapturedEvent?.Invoke();
+ }
+
+ // The context is not lost/reset in the windowed in-process model.
+ public void TriggerDeviceReset() { }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLHwndHostWindow.cs b/UIX.RenderApi.OpenGL/Engine/GLHwndHostWindow.cs
new file mode 100644
index 0000000..3cc60db
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLHwndHostWindow.cs
@@ -0,0 +1,22 @@
+using System;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Placeholder host-window used to embed native HWND content inside the scene.
+ /// Cross-platform HWND hosting has no in-process GL equivalent, so this tracks
+ /// state only. Stage-3 TODO: platform-gated child-surface embedding.
+ ///
+ public sealed class GLHwndHostWindow : IHwndHostWindow
+ {
+ public ColorF BackgroundColor { get; set; }
+ public Point ClientPosition { get; set; }
+ public IntPtr Hwnd => IntPtr.Zero;
+ public bool Visible { get; set; }
+ public Size WindowSize { get; set; }
+
+ public event EventHandler? OnHandleChanged;
+
+ public void Dispose() => OnHandleChanged = null;
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs
new file mode 100644
index 0000000..1ac3f41
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLInputSystem.cs
@@ -0,0 +1,15 @@
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Holds the current raw-input callback sink. The window (see )
+ /// translates Silk.NET input events and dispatches them here.
+ ///
+ public sealed class GLInputSystem : IInputSystem
+ {
+ public IRawInputCallbacks? Callbacks { get; private set; }
+
+ public void RegisterRawInputCallbacks(IRawInputCallbacks handlers) => Callbacks = handlers;
+
+ public void UnregisterRawInputCallbacks() => Callbacks = null;
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs
new file mode 100644
index 0000000..e99e76b
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLRenderEngine.cs
@@ -0,0 +1,131 @@
+using System;
+using System.Threading;
+using Silk.NET.Maths;
+using Silk.NET.OpenGL;
+using Silk.NET.Windowing;
+// Disambiguate from Microsoft.Iris.Render.WindowOptions (enum) and this class's Window property.
+using SilkWindowOptions = Silk.NET.Windowing.WindowOptions;
+using SilkWindowFactory = Silk.NET.Windowing.Window;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// In-process, OpenGL-backed render engine. Owns a single Silk.NET window and its GL
+ /// context, pumps native events on demand and renders the visual tree each frame.
+ ///
+ public sealed class GLRenderEngine : IRenderEngine
+ {
+ private readonly IRenderHost m_host;
+ private readonly IWindow m_silkWindow;
+ private readonly GLRenderSession m_session;
+ private readonly GLRenderWindow m_window;
+
+ private GL? m_gl;
+ private SceneRenderer? m_renderer;
+ private GLDisplayManager? m_displayManager;
+
+ private GraphicsRenderingQuality m_quality;
+ private SoundDeviceType m_soundType;
+ private volatile bool m_wakeRequested;
+
+ public GLRenderEngine(IrisEngineInfo engineInfo, IRenderHost renderHost)
+ {
+ m_host = renderHost;
+
+ var options = SilkWindowOptions.Default;
+ options.Title = "Iris";
+ options.API = new GraphicsAPI(
+ ContextAPI.OpenGL, ContextProfile.Core, ContextFlags.Default, new APIVersion(3, 3));
+ options.IsVisible = true;
+ options.ShouldSwapAutomatically = true;
+ m_silkWindow = SilkWindowFactory.Create(options);
+
+ m_session = new GLRenderSession();
+ m_window = new GLRenderWindow(m_silkWindow, m_session);
+
+ m_silkWindow.Load += OnLoad;
+ m_silkWindow.Render += OnRender;
+ m_silkWindow.Resize += _ => m_window.RaiseResize();
+ m_silkWindow.Move += _ => m_window.RaiseMove();
+ m_silkWindow.Closing += m_window.RaiseClose;
+ m_silkWindow.FocusChanged += m_window.RaiseActivation;
+ }
+
+ public IRenderSession Session => m_session;
+ public IRenderWindow Window => m_window;
+ public IDisplayManager DisplayManager => m_displayManager ?? throw new InvalidOperationException("Engine not initialized");
+
+ public void Initialize(
+ GraphicsDeviceType typeGraphics,
+ GraphicsRenderingQuality renderingQuality,
+ SoundDeviceType typeSound)
+ {
+ m_quality = renderingQuality;
+ m_soundType = typeSound;
+ // Window/GL creation is completed when GLRenderWindow.Initialize() runs and
+ // Silk raises Load (see OnLoad); we only latch the requested settings here.
+ }
+
+ private void OnLoad()
+ {
+ m_gl = GL.GetApi(m_silkWindow);
+ m_renderer = new SceneRenderer(m_gl);
+ m_displayManager = new GLDisplayManager(m_silkWindow);
+ m_session.GraphicsDevice = new GLGraphicsDevice(m_gl, m_quality, RenderNow);
+ m_session.SoundDevice = new GLSoundDevice(m_soundType);
+ m_window.RaiseLoad();
+ }
+
+ private void OnRender(double deltaSeconds)
+ {
+ if (m_renderer == null)
+ return;
+ m_renderer.BeginFrame(m_window.Width, m_window.Height, m_window.BackgroundColor);
+ m_window.Root.Render(m_renderer, Matrix4X4.Identity, 1f);
+ }
+
+ private void RenderNow()
+ {
+ if (m_window.IsLoaded && !m_silkWindow.IsClosing)
+ m_silkWindow.DoRender();
+ }
+
+ public bool ProcessNativeEvents()
+ {
+ m_silkWindow.DoEvents();
+ return !m_silkWindow.IsClosing;
+ }
+
+ public void WaitForWork(uint nTimeoutInMsecs)
+ {
+ // Cooperative wait: return promptly if another thread requested a wake.
+ uint waited = 0;
+ const uint slice = 5;
+ while (waited < nTimeoutInMsecs && !m_wakeRequested)
+ {
+ Thread.Sleep((int)Math.Min(slice, nTimeoutInMsecs - waited));
+ waited += slice;
+ }
+ m_wakeRequested = false;
+ }
+
+ public void InterThreadWake() => m_wakeRequested = true;
+
+ public void FlushBatch() => RenderNow();
+
+ public bool IsGraphicsDeviceAvailable(GraphicsDeviceType type, bool fFilterRecommended)
+ => type == GraphicsDeviceType.Direct3D9 || type == GraphicsDeviceType.Gdi;
+
+ public bool IsSoundDeviceAvailable(SoundDeviceType type)
+ => type == SoundDeviceType.WaveAudio || type == SoundDeviceType.DirectSound8;
+
+ public void Dispose()
+ {
+ m_renderer?.Dispose();
+ m_session.Dispose();
+ if (!m_silkWindow.IsClosing)
+ m_silkWindow.Close();
+ m_silkWindow.Dispose();
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderSession.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderSession.cs
new file mode 100644
index 0000000..31763ec
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLRenderSession.cs
@@ -0,0 +1,74 @@
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Thin factory that produces the render objects (visuals, sprites, images, effects,
+ /// …) and exposes the animation/input/graphics/sound subsystems. All objects live in
+ /// the same process and share the engine's single GL context.
+ ///
+ public sealed class GLRenderSession : IRenderSession
+ {
+ public GLRenderSession()
+ {
+ AnimationSystem = new GLAnimationSystem();
+ InputSystem = new GLInputSystem();
+ }
+
+ public IAnimationSystem AnimationSystem { get; }
+ public IInputSystem InputSystem { get; }
+ public IGraphicsDevice GraphicsDevice { get; internal set; } = null!;
+ public ISoundDevice SoundDevice { get; internal set; } = null!;
+
+ internal GLInputSystem RawInput => (GLInputSystem)InputSystem;
+
+ public IEffectTemplate CreateEffectTemplate(object objUser, string stName)
+ {
+ var t = new GLEffectTemplate(stName);
+ t.RegisterUsage(objUser);
+ return t;
+ }
+
+ public IVideoStream CreateVideoStream(object objUser)
+ {
+ var v = new GLVideoStream();
+ v.RegisterUsage(objUser);
+ return v;
+ }
+
+ public IVisualContainer CreateVisualContainer(object objUser, object objOwnerData)
+ {
+ var c = new GLVisualContainer(this, objOwnerData, isRoot: false);
+ c.RegisterUsage(objUser);
+ return c;
+ }
+
+ public ICamera CreateCamera(object objUser)
+ {
+ var c = new GLCamera();
+ c.RegisterUsage(objUser);
+ return c;
+ }
+
+ public IGradient CreateGradient(object objUser)
+ {
+ var g = new GLGradient();
+ g.RegisterUsage(objUser);
+ return g;
+ }
+
+ public ISprite CreateSprite(object objUser, object objOwnerData)
+ {
+ var s = new GLSprite(this, objOwnerData);
+ s.RegisterUsage(objUser);
+ return s;
+ }
+
+ public IImage CreateImage(object objUser, string identifier, ContentNotifyHandler handler)
+ {
+ var img = new GLImage(identifier, handler);
+ img.RegisterUsage(objUser);
+ return img;
+ }
+
+ public void Dispose() { }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs
new file mode 100644
index 0000000..218c250
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Engine/GLRenderWindow.cs
@@ -0,0 +1,193 @@
+using System;
+using Microsoft.Iris.Input;
+using Silk.NET.Maths;
+using SilkWindow = Silk.NET.Windowing.IWindow;
+using SilkWindowState = Silk.NET.Windowing.WindowState;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// implemented over a Silk.NET window. Owns the root
+ /// visual container and re-raises the Iris window events from Silk callbacks.
+ ///
+ public sealed class GLRenderWindow : IRenderWindow
+ {
+ private readonly SilkWindow m_window;
+ private readonly GLVisualContainer m_root;
+ private Size m_initialClientSize = new Size(1024, 768);
+
+ public GLRenderWindow(SilkWindow window, GLRenderSession session)
+ {
+ m_window = window;
+ m_root = new GLVisualContainer(session, null!, isRoot: true);
+ m_root.RegisterUsage(this);
+ }
+
+ internal GLVisualContainer Root => m_root;
+
+ // ---- Geometry ------------------------------------------------------------
+ public int Left => m_window.Position.X;
+ public int Top => m_window.Position.Y;
+ public int Right => m_window.Position.X + m_window.Size.X;
+ public int Bottom => m_window.Position.Y + m_window.Size.Y;
+ public int Width => m_window.Size.X;
+ public int Height => m_window.Size.Y;
+
+ public HWND WindowHandle => new HWND(m_window.Native?.Win32?.Hwnd ?? IntPtr.Zero);
+
+ public Size ClientSize
+ {
+ get => new Size(m_window.Size.X, m_window.Size.Y);
+ set => m_window.Size = new Vector2D(value.Width, value.Height);
+ }
+
+ public Size InitialClientSize
+ {
+ get => m_initialClientSize;
+ set => m_initialClientSize = value;
+ }
+
+ public FormPlacement InitialPlacement
+ {
+ set { /* honored at creation time via InitialClientSize/Position */ }
+ }
+
+ public FormPlacement FinalPlacement => new FormPlacement
+ {
+ NormalPosition = new Rectangle(Left, Top, Width, Height),
+ MaximizedLocation = Point.Zero,
+ ShowState = (uint)m_window.WindowState,
+ };
+
+ public int MinResizeWidth { get; set; }
+ public int MaxResizeWidth { get; set; }
+
+ public Point Position
+ {
+ get => new Point(m_window.Position.X, m_window.Position.Y);
+ set => m_window.Position = new Vector2D(value.X, value.Y);
+ }
+
+ public string Text
+ {
+ get => m_window.Title;
+ set => m_window.Title = value ?? string.Empty;
+ }
+
+ public Cursor Cursor { get; set; } = Cursor.Default;
+ public Cursor IdleCursor { get; set; } = Cursor.Default;
+
+ public bool Visible
+ {
+ get => m_window.IsVisible;
+ set => m_window.IsVisible = value;
+ }
+
+ public bool IsLoaded { get; internal set; }
+ public ColorF BackgroundColor { get; set; } = new ColorF(0f, 0f, 0f, 1f);
+ public bool EnableExternalDragDrop { get; set; }
+ public bool IsDragInProgress { get; set; }
+ public IDisplay? CurrentDisplay { get; set; }
+ public bool FullScreenExclusive { get; set; }
+ public bool ActivationState { get; internal set; } = true;
+
+ public WindowState WindowState
+ {
+ get => m_window.WindowState switch
+ {
+ SilkWindowState.Minimized => WindowState.Minimized,
+ SilkWindowState.Maximized => WindowState.Maximized,
+ _ => WindowState.Normal,
+ };
+ set => m_window.WindowState = value switch
+ {
+ WindowState.Minimized => SilkWindowState.Minimized,
+ WindowState.Maximized => SilkWindowState.Maximized,
+ _ => SilkWindowState.Normal,
+ };
+ }
+
+ public FormStyleInfo Styles { get; set; }
+ public HWND AppNotifyWindow { set { /* native app-notify sink; unused in-process */ } }
+
+ public IVisualContainer VisualRoot => m_root;
+
+ // ---- Methods -------------------------------------------------------------
+ public void Initialize()
+ {
+ // Creates the OS window + GL context; Silk raises Load, which the engine
+ // handles to build the GL device and then re-raises LoadEvent.
+ m_window.Size = new Vector2D(m_initialClientSize.Width, m_initialClientSize.Height);
+ m_window.Initialize();
+ }
+
+ public void SetIcon(string sModuleName, uint nResourceID, IconFlags nOptions) { /* TODO(stage 3): load icon */ }
+ public void SetEdgeImages(bool fActiveEdges, ShadowEdgePart[] edges) { /* TODO(stage 3): window shadow edges */ }
+ public void SetWindowOptions(WindowOptions options, bool enable) { /* TODO(stage 3): map to Silk window flags */ }
+ public void SetMouseIdleOptions(Size sizeMouseIdleTolerance, uint nMouseIdleDelay) { }
+ public void SetCapture(IRawInputSite captureSite, bool state) { }
+ public void SetDragDropResult(uint nDragOverResult, uint nDragDropResult) { }
+
+ public void ClientToScreen(ref Point point)
+ {
+ point = new Point(point.X + Left, point.Y + Top);
+ }
+
+ public void ScreenToClient(ref Point point)
+ {
+ point = new Point(point.X - Left, point.Y - Top);
+ }
+
+ public void ForceMouseIdle(bool fIdle) => MouseIdleEvent?.Invoke(fIdle);
+ public void LockMouseActive(bool fActive) { }
+ public void RefreshHitTarget() { }
+ public void TakeFocus() { }
+ public void TakeForeground(bool fForce) { }
+ public void BringToTop() { }
+ public void Restore() => WindowState = WindowState.Normal;
+ public void TemporarilyExitExclusiveMode() { }
+
+ public void Close(FormCloseReason fcrCloseReason)
+ {
+ CloseRequestEvent?.Invoke();
+ m_window.Close();
+ }
+
+ public IHwndHostWindow CreateHwndHostWindow() => new GLHwndHostWindow();
+
+ // ---- Events --------------------------------------------------------------
+ public event LocationChangedHandler? LocationChangedEvent;
+ public event SizeChangedHandler? SizeChangedEvent;
+ public event MonitorChangedHandler? MonitorChangedEvent;
+ public event WindowStateChangedHandler? WindowStateChangedEvent;
+ public event SysCommandHandler? SysCommandEvent;
+ public event MouseIdleHandler? MouseIdleEvent;
+ public event ShowHandler? ShowEvent;
+ public event ActivationChangeHandler? ActivationChangeEvent;
+ public event SessionActivateHandler? SessionActivateEvent;
+ public event SessionConnectHandler? SessionConnectEvent;
+ public event SetFocusHandler? SetFocusEvent;
+ public event LoadHandler? LoadEvent;
+ public event CloseHandler? CloseEvent;
+ public event CloseRequestHandler? CloseRequestEvent;
+ public event ForwardMessageHandler? ForwardMessageEvent;
+
+ // Raisers invoked by the engine as it pumps the Silk window.
+ internal void RaiseLoad()
+ {
+ IsLoaded = true;
+ LoadEvent?.Invoke();
+ ShowEvent?.Invoke(true, true);
+ }
+
+ internal void RaiseResize() => SizeChangedEvent?.Invoke();
+ internal void RaiseMove() => LocationChangedEvent?.Invoke(Position);
+ internal void RaiseClose() => CloseEvent?.Invoke();
+
+ internal void RaiseActivation(bool active)
+ {
+ ActivationState = active;
+ ActivationChangeEvent?.Invoke();
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/OpenGLRenderApi.cs b/UIX.RenderApi.OpenGL/OpenGLRenderApi.cs
new file mode 100644
index 0000000..8c48a52
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/OpenGLRenderApi.cs
@@ -0,0 +1,31 @@
+using System;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Entry point for the in-process, OpenGL-backed renderer. Mirrors
+ /// ; used instead of it
+ /// because RenderApi is fixed to the native Iris engine and must not change.
+ ///
+ public static class OpenGLRenderApi
+ {
+ /// Create an OpenGL render engine for the given Iris engine info.
+ public static IRenderEngine CreateEngine(IrisEngineInfo engineInfo, IRenderHost renderHost)
+ {
+ if (engineInfo == null) throw new ArgumentNullException(nameof(engineInfo));
+ if (renderHost == null) throw new ArgumentNullException(nameof(renderHost));
+ return new GLRenderEngine(engineInfo, renderHost);
+ }
+
+ ///
+ /// Convenience overload matching RenderApi.CreateEngine's signature.
+ /// Only is supported (the sole EngineType).
+ ///
+ public static IRenderEngine CreateEngine(EngineInfo engineInfo, IRenderHost renderHost)
+ {
+ if (engineInfo is not IrisEngineInfo iris)
+ throw new ArgumentException("Only IrisEngineInfo is supported.", nameof(engineInfo));
+ return CreateEngine(iris, renderHost);
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Rendering/SceneRenderer.cs b/UIX.RenderApi.OpenGL/Rendering/SceneRenderer.cs
new file mode 100644
index 0000000..f898d23
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Rendering/SceneRenderer.cs
@@ -0,0 +1,202 @@
+using System;
+using Silk.NET.Maths;
+using Silk.NET.OpenGL;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Immediate-mode style GL renderer for the visual tree. Draws each sprite as a quad
+ /// in an orthographic, top-left-origin, pixel-space projection with alpha blending.
+ ///
+ internal sealed unsafe class SceneRenderer : IDisposable
+ {
+ private const string VertexSource = @"#version 330 core
+layout(location = 0) in vec2 aPos;
+layout(location = 1) in vec2 aTex;
+uniform mat4 uModel;
+uniform mat4 uProj;
+uniform vec2 uSize;
+out vec2 vTex;
+void main()
+{
+ vTex = aTex;
+ gl_Position = uProj * uModel * vec4(aPos * uSize, 0.0, 1.0);
+}";
+
+ private const string FragmentSource = @"#version 330 core
+in vec2 vTex;
+out vec4 fragColor;
+uniform sampler2D uTex;
+uniform int uUseTexture;
+uniform vec4 uColor;
+uniform float uAlpha;
+void main()
+{
+ if (uUseTexture == 1)
+ {
+ vec4 t = texture(uTex, vTex);
+ fragColor = vec4(t.rgb, t.a * uAlpha);
+ }
+ else
+ {
+ fragColor = vec4(uColor.rgb, uColor.a * uAlpha);
+ }
+}";
+
+ private readonly GL m_gl;
+ private readonly uint m_program;
+ private readonly uint m_vao;
+ private readonly uint m_vbo;
+
+ private readonly int m_locModel;
+ private readonly int m_locProj;
+ private readonly int m_locSize;
+ private readonly int m_locUseTexture;
+ private readonly int m_locColor;
+ private readonly int m_locAlpha;
+ private readonly int m_locTex;
+
+ private Matrix4X4 m_projection = Matrix4X4.Identity;
+
+ public SceneRenderer(GL gl)
+ {
+ m_gl = gl;
+
+ m_program = BuildProgram(gl);
+ m_locModel = gl.GetUniformLocation(m_program, "uModel");
+ m_locProj = gl.GetUniformLocation(m_program, "uProj");
+ m_locSize = gl.GetUniformLocation(m_program, "uSize");
+ m_locUseTexture = gl.GetUniformLocation(m_program, "uUseTexture");
+ m_locColor = gl.GetUniformLocation(m_program, "uColor");
+ m_locAlpha = gl.GetUniformLocation(m_program, "uAlpha");
+ m_locTex = gl.GetUniformLocation(m_program, "uTex");
+
+ // Unit quad: interleaved position (xy) + texcoord (uv). Texcoords are
+ // y-flipped so BGRA image rows (top-down) map upright in our y-down space.
+ float[] verts =
+ {
+ 0f, 0f, 0f, 0f,
+ 1f, 0f, 1f, 0f,
+ 1f, 1f, 1f, 1f,
+ 0f, 0f, 0f, 0f,
+ 1f, 1f, 1f, 1f,
+ 0f, 1f, 0f, 1f,
+ };
+
+ m_vao = gl.GenVertexArray();
+ gl.BindVertexArray(m_vao);
+ m_vbo = gl.GenBuffer();
+ gl.BindBuffer(BufferTargetARB.ArrayBuffer, m_vbo);
+ fixed (float* v = verts)
+ {
+ gl.BufferData(BufferTargetARB.ArrayBuffer, (nuint)(verts.Length * sizeof(float)), v, BufferUsageARB.StaticDraw);
+ }
+ gl.EnableVertexAttribArray(0);
+ gl.VertexAttribPointer(0, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)0);
+ gl.EnableVertexAttribArray(1);
+ gl.VertexAttribPointer(1, 2, VertexAttribPointerType.Float, false, 4 * sizeof(float), (void*)(2 * sizeof(float)));
+ gl.BindVertexArray(0);
+ }
+
+ public void BeginFrame(int widthPixels, int heightPixels, ColorF clear)
+ {
+ m_projection = Matrix4X4.CreateOrthographicOffCenter(0f, widthPixels, heightPixels, 0f, -1f, 1f);
+
+ m_gl.Viewport(0, 0, (uint)Math.Max(1, widthPixels), (uint)Math.Max(1, heightPixels));
+ m_gl.Disable(EnableCap.DepthTest);
+ m_gl.Enable(EnableCap.Blend);
+ m_gl.BlendFunc(BlendingFactor.SrcAlpha, BlendingFactor.OneMinusSrcAlpha);
+ m_gl.ClearColor(clear.R, clear.G, clear.B, clear.A);
+ m_gl.Clear((uint)ClearBufferMask.ColorBufferBit);
+
+ m_gl.UseProgram(m_program);
+ UploadMatrix(m_locProj, m_projection);
+ }
+
+ public void DrawColoredQuad(Matrix4X4 model, float width, float height, ColorF color, float alpha)
+ {
+ m_gl.UseProgram(m_program);
+ UploadMatrix(m_locModel, model);
+ m_gl.Uniform2(m_locSize, width, height);
+ m_gl.Uniform1(m_locUseTexture, 0);
+ m_gl.Uniform4(m_locColor, color.R, color.G, color.B, color.A);
+ m_gl.Uniform1(m_locAlpha, alpha);
+ DrawQuad();
+ }
+
+ public void DrawTexturedQuad(Matrix4X4 model, float width, float height, GLImage image, float alpha)
+ {
+ image.EnsureUploaded(m_gl);
+ if (image.TextureId == 0)
+ return;
+
+ m_gl.UseProgram(m_program);
+ UploadMatrix(m_locModel, model);
+ m_gl.Uniform2(m_locSize, width, height);
+ m_gl.Uniform1(m_locUseTexture, 1);
+ m_gl.Uniform1(m_locAlpha, alpha);
+ m_gl.ActiveTexture(TextureUnit.Texture0);
+ m_gl.BindTexture(TextureTarget.Texture2D, image.TextureId);
+ m_gl.Uniform1(m_locTex, 0);
+ DrawQuad();
+ }
+
+ private void DrawQuad()
+ {
+ m_gl.BindVertexArray(m_vao);
+ m_gl.DrawArrays(PrimitiveType.Triangles, 0, 6);
+ m_gl.BindVertexArray(0);
+ }
+
+ // Silk.NET.Maths stores matrices row-major; uploading with transpose=false makes
+ // GLSL read the transpose, so the shader uses column-vector order uProj*uModel*v.
+ private void UploadMatrix(int location, Matrix4X4 m)
+ {
+ float[] a =
+ {
+ m.M11, m.M12, m.M13, m.M14,
+ m.M21, m.M22, m.M23, m.M24,
+ m.M31, m.M32, m.M33, m.M34,
+ m.M41, m.M42, m.M43, m.M44,
+ };
+ fixed (float* p = a)
+ m_gl.UniformMatrix4(location, 1, false, p);
+ }
+
+ private static uint BuildProgram(GL gl)
+ {
+ uint vs = CompileShader(gl, ShaderType.VertexShader, VertexSource);
+ uint fs = CompileShader(gl, ShaderType.FragmentShader, FragmentSource);
+ uint program = gl.CreateProgram();
+ gl.AttachShader(program, vs);
+ gl.AttachShader(program, fs);
+ gl.LinkProgram(program);
+ gl.GetProgram(program, ProgramPropertyARB.LinkStatus, out int linked);
+ if (linked == 0)
+ throw new InvalidOperationException("Shader link failed: " + gl.GetProgramInfoLog(program));
+ gl.DetachShader(program, vs);
+ gl.DetachShader(program, fs);
+ gl.DeleteShader(vs);
+ gl.DeleteShader(fs);
+ return program;
+ }
+
+ private static uint CompileShader(GL gl, ShaderType type, string source)
+ {
+ uint shader = gl.CreateShader(type);
+ gl.ShaderSource(shader, source);
+ gl.CompileShader(shader);
+ gl.GetShader(shader, ShaderParameterName.CompileStatus, out int status);
+ if (status == 0)
+ throw new InvalidOperationException($"{type} compile failed: " + gl.GetShaderInfoLog(shader));
+ return shader;
+ }
+
+ public void Dispose()
+ {
+ m_gl.DeleteBuffer(m_vbo);
+ m_gl.DeleteVertexArray(m_vao);
+ m_gl.DeleteProgram(m_program);
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLCamera.cs b/UIX.RenderApi.OpenGL/Scene/GLCamera.cs
new file mode 100644
index 0000000..7da9a19
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLCamera.cs
@@ -0,0 +1,16 @@
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Camera parameters for a visual container. Stored verbatim; the current renderer
+ /// composites in an orthographic screen space, so perspective cameras are recorded
+ /// but not yet applied (stage-3 TODO for true 3D containers).
+ ///
+ public sealed class GLCamera : SharedRenderObject, ICamera
+ {
+ public Vector3 Eye { get; set; }
+ public Vector3 At { get; set; }
+ public Vector3 Up { get; set; } = new Vector3(0f, 1f, 0f);
+ public float Zn { get; set; } = 1f;
+ public bool Perspective { get; set; }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLEffect.cs b/UIX.RenderApi.OpenGL/Scene/GLEffect.cs
new file mode 100644
index 0000000..e635743
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLEffect.cs
@@ -0,0 +1,69 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Template describing an effect (pixel-shader program in the original renderer).
+ /// We treat effects as typed property bags; the built-in image path reads the first
+ /// image property. Custom shader compilation is a stage-3 TODO.
+ ///
+ public sealed class GLEffectTemplate : SharedRenderObject, IEffectTemplate
+ {
+ private readonly List m_properties = new List();
+
+ public GLEffectTemplate(string name) => Name = name;
+
+ public string Name { get; }
+ public bool IsBuilt { get; private set; }
+
+ public void AddEffectProperty(string stPath) => m_properties.Add(stPath);
+
+ public bool Build(EffectInput input)
+ {
+ // No shader compilation yet; mark as built so callers proceed. The image
+ // path in GLSprite does not depend on a compiled program.
+ IsBuilt = true;
+ return true;
+ }
+
+ public IEffect CreateInstance(object objUser) => new GLEffect(this);
+ }
+
+ /// Instance of an holding property values.
+ public sealed class GLEffect : SharedRenderObject, IEffect
+ {
+ private readonly Dictionary m_values = new Dictionary();
+
+ public GLEffect(GLEffectTemplate template) => Template = template;
+
+ public string Name => Template.Name;
+ IEffectTemplate IEffect.Template => Template;
+ public GLEffectTemplate Template { get; }
+
+ public void SetProperty(string stPropertyName, int nValue) => m_values[stPropertyName] = nValue;
+ public void SetProperty(string stPropertyName, float flValue) => m_values[stPropertyName] = flValue;
+ public void SetProperty(string stPropertyName, Vector2 vValue) => m_values[stPropertyName] = vValue;
+ public void SetProperty(string stPropertyName, Vector3 vValue) => m_values[stPropertyName] = vValue;
+ public void SetProperty(string stPropertyName, Vector4 vValue) => m_values[stPropertyName] = vValue;
+ public void SetProperty(string stPropertyName, ColorF colorValue) => m_values[stPropertyName] = colorValue;
+ public void SetProperty(string stPropertyName, IImage imgValue) => m_values[stPropertyName] = imgValue;
+ public void SetProperty(string stPropertyName, IImage[] imgValue) => m_values[stPropertyName] = imgValue;
+ public void SetProperty(string stPropertyName, IVideoStream streamValue) => m_values[stPropertyName] = streamValue;
+
+ /// First image assigned to any property, used as the sprite's texture.
+ internal GLImage? PrimaryImage
+ {
+ get
+ {
+ foreach (object v in m_values.Values)
+ {
+ if (v is GLImage img)
+ return img;
+ if (v is IImage[] arr && arr.Length > 0 && arr[0] is GLImage first)
+ return first;
+ }
+ return null;
+ }
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLGradient.cs b/UIX.RenderApi.OpenGL/Scene/GLGradient.cs
new file mode 100644
index 0000000..943928e
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLGradient.cs
@@ -0,0 +1,37 @@
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// An alpha/color gradient that can be attached to a visual. We record the stops and
+ /// mask; applying them as an alpha ramp during compositing is left as a stage-3 TODO.
+ ///
+ public sealed class GLGradient : SharedRenderObject, IGradient
+ {
+ internal readonly struct Stop
+ {
+ public readonly float Position;
+ public readonly float Value;
+ public readonly RelativeSpace Space;
+ public Stop(float position, float value, RelativeSpace space)
+ {
+ Position = position;
+ Value = value;
+ Space = space;
+ }
+ }
+
+ private readonly List m_stops = new List();
+
+ public Orientation Orientation { get; set; }
+ public ColorF ColorMask { get; set; } = new ColorF(1f, 1f, 1f, 1f);
+ public float Offset { get; set; }
+
+ public void AddValue(float flPosition, float flValue, RelativeSpace rsSpace)
+ => m_stops.Add(new Stop(flPosition, flValue, rsSpace));
+
+ public void Clear() => m_stops.Clear();
+
+ internal IReadOnlyList Stops => m_stops;
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLImage.cs b/UIX.RenderApi.OpenGL/Scene/GLImage.cs
new file mode 100644
index 0000000..c655e15
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLImage.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Runtime.InteropServices;
+using Silk.NET.OpenGL;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// An image backed by an OpenGL texture. Pixel content supplied through
+ /// is copied into managed memory and uploaded lazily on
+ /// the render thread (GL calls are only valid there), then cached as a texture.
+ ///
+ public sealed class GLImage : SharedRenderObject, IImage
+ {
+ private readonly ContentNotifyHandler? m_notify;
+ private byte[]? m_pixelsBgra; // always stored as tightly-packed BGRA (A8R8G8B8 little-endian)
+ private bool m_dirty;
+ private uint m_texture;
+
+ public GLImage(string identifier, ContentNotifyHandler? notify)
+ {
+ Identifier = identifier;
+ m_notify = notify;
+ }
+
+ public Size Size { get; private set; }
+ public ImageFormat Format { get; private set; } = ImageFormat.None;
+ public string Identifier { get; }
+
+ internal uint TextureId => m_texture;
+
+ public bool LoadContent(ImageFormat format, Size size, int Stride, IntPtr Data)
+ {
+ if (Data == IntPtr.Zero || size.Width <= 0 || size.Height <= 0)
+ return false;
+
+ int bpp = format == ImageFormat.A8 ? 1 : 4;
+ byte[] packed = new byte[size.Width * size.Height * 4];
+
+ for (int y = 0; y < size.Height; y++)
+ {
+ IntPtr row = Data + y * Stride;
+ for (int x = 0; x < size.Width; x++)
+ {
+ int dst = (y * size.Width + x) * 4;
+ if (format == ImageFormat.A8)
+ {
+ byte a = Marshal.ReadByte(row, x);
+ packed[dst + 0] = 255;
+ packed[dst + 1] = 255;
+ packed[dst + 2] = 255;
+ packed[dst + 3] = a;
+ }
+ else
+ {
+ int src = x * bpp;
+ byte b = Marshal.ReadByte(row, src + 0);
+ byte g = Marshal.ReadByte(row, src + 1);
+ byte r = Marshal.ReadByte(row, src + 2);
+ byte a = format == ImageFormat.X8R8G8B8 ? (byte)255 : Marshal.ReadByte(row, src + 3);
+ packed[dst + 0] = b;
+ packed[dst + 1] = g;
+ packed[dst + 2] = r;
+ packed[dst + 3] = a;
+ }
+ }
+ }
+
+ m_pixelsBgra = packed;
+ Size = size;
+ Format = format;
+ m_dirty = true;
+ m_notify?.Invoke(ContentNotification.Acquire, this, Data);
+ return true;
+ }
+
+ /// Upload pending pixel content to the GPU. Must run on the GL thread.
+ internal unsafe void EnsureUploaded(GL gl)
+ {
+ if (!m_dirty || m_pixelsBgra == null)
+ return;
+ m_dirty = false;
+
+ if (m_texture == 0)
+ m_texture = gl.GenTexture();
+
+ gl.BindTexture(TextureTarget.Texture2D, m_texture);
+ gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMinFilter, (int)GLEnum.Linear);
+ gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureMagFilter, (int)GLEnum.Linear);
+ gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapS, (int)GLEnum.ClampToEdge);
+ gl.TexParameter(TextureTarget.Texture2D, TextureParameterName.TextureWrapT, (int)GLEnum.ClampToEdge);
+
+ fixed (byte* p = m_pixelsBgra)
+ {
+ gl.TexImage2D(TextureTarget.Texture2D, 0, InternalFormat.Rgba,
+ (uint)Size.Width, (uint)Size.Height, 0,
+ PixelFormat.Bgra, PixelType.UnsignedByte, p);
+ }
+ gl.BindTexture(TextureTarget.Texture2D, 0);
+ }
+
+ internal void DeleteTexture(GL gl)
+ {
+ if (m_texture != 0)
+ {
+ gl.DeleteTexture(m_texture);
+ m_texture = 0;
+ }
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLSprite.cs b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs
new file mode 100644
index 0000000..8b19501
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLSprite.cs
@@ -0,0 +1,61 @@
+using Silk.NET.Maths;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// A leaf visual that paints a textured/colored quad. Content comes from its
+ /// (typically an image effect); absent an image it fills
+ /// with the sprite's debug color, which keeps placeholder content visible.
+ ///
+ public sealed class GLSprite : GLVisual, ISprite
+ {
+ private int m_nineGridLeft, m_nineGridTop, m_nineGridRight, m_nineGridBottom;
+
+ public GLSprite(GLRenderSession session, object ownerData)
+ : base(session, ownerData)
+ {
+ }
+
+ public IEffect? Effect { get; set; }
+ public bool RelativeSize { get; set; }
+
+ public void SetCoordMap(int idxLayer, CoordMap coordMap)
+ {
+ // TODO(stage 3): honor per-layer coordinate remaps. Not required for the
+ // basic textured-quad path; stored intent is dropped for now.
+ }
+
+ public void SetNineGrid(int left, int top, int right, int bottom)
+ {
+ m_nineGridLeft = left;
+ m_nineGridTop = top;
+ m_nineGridRight = right;
+ m_nineGridBottom = bottom;
+ // TODO(stage 3): implement 9-slice stretching. Currently the sprite is
+ // drawn as a single stretched quad regardless of these insets.
+ }
+
+ internal override void Render(SceneRenderer renderer, Matrix4X4 parentMatrix, float inheritedAlpha)
+ {
+ if (!Visible || Size.X <= 0f || Size.Y <= 0f)
+ return;
+
+ Matrix4X4 matrix = LocalMatrix * parentMatrix;
+ float alpha = inheritedAlpha * Alpha;
+
+ GLImage? image = (Effect as GLEffect)?.PrimaryImage;
+ if (image != null)
+ {
+ renderer.DrawTexturedQuad(matrix, Size.X, Size.Y, image, alpha);
+ }
+ else
+ {
+ ColorF c = DebugColor;
+ // A zeroed ColorF would be fully transparent black; only draw when the
+ // caller actually assigned a debug color.
+ if (c.A > 0f)
+ renderer.DrawColoredQuad(matrix, Size.X, Size.Y, c, alpha);
+ }
+ }
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLVideoStream.cs b/UIX.RenderApi.OpenGL/Scene/GLVideoStream.cs
new file mode 100644
index 0000000..f429421
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLVideoStream.cs
@@ -0,0 +1,24 @@
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Placeholder video stream. Metadata is tracked but no decoding/presentation is
+ /// wired up yet (stage-3 TODO: back this with a media pipeline and a GL texture).
+ ///
+ public sealed class GLVideoStream : SharedRenderObject, IVideoStream
+ {
+ private static int s_nextId = 1;
+
+ public GLVideoStream() => StreamID = s_nextId++;
+
+ public int StreamID { get; }
+ public float ContentOverscan { get; set; }
+ public int ContentAspectWidth => ContentWidth;
+ public int ContentAspectHeight => ContentHeight;
+ public int ContentHeight { get; internal set; }
+ public int ContentWidth { get; internal set; }
+
+ public event InvalidateContentHandler? InvalidateContentEvent;
+
+ internal void RaiseInvalidateContent() => InvalidateContentEvent?.Invoke();
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisual.cs b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs
new file mode 100644
index 0000000..9e308d2
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLVisual.cs
@@ -0,0 +1,120 @@
+using System.Collections.Generic;
+using Silk.NET.Maths;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Base class shared by and .
+ /// Holds the common 2.5D transform state (position/size/scale/rotation/alpha) and
+ /// produces the local model matrix used when walking the tree during rendering.
+ ///
+ public abstract class GLVisual : SharedRenderObject, IVisual
+ {
+ private readonly object m_ownerData;
+ protected readonly GLRenderSession Session;
+
+ private Vector3 m_position = Vector3.Zero;
+ private Vector2 m_size = Vector2.Zero;
+ private Vector3 m_scale = Vector3.UnitVector;
+ private AxisAngle m_rotation = AxisAngle.Identity;
+ private Vector3 m_centerPoint = Vector3.Zero;
+ private float m_alpha = 1f;
+ private bool m_visible = true;
+ private uint m_layer;
+
+ private readonly List m_gradients = new List();
+
+ protected GLVisual(GLRenderSession session, object ownerData)
+ {
+ Session = session;
+ m_ownerData = ownerData;
+ }
+
+ // IRawInputSite
+ public object OwnerData => m_ownerData;
+
+ // IVisual
+ public MouseOptions MouseOptions { get; set; } = MouseOptions.None;
+ public GLVisualContainer? ParentContainer { get; internal set; }
+ public IVisualContainer Parent => ParentContainer!;
+ public string DebugID { get; set; } = string.Empty;
+ public ColorF DebugColor { get; set; }
+
+ public void Remove() => ParentContainer?.RemoveChild(this);
+
+ public virtual void CopyFrom(IVisual visualSource)
+ {
+ if (visualSource is not GLVisual src)
+ return;
+ m_position = src.m_position;
+ m_size = src.m_size;
+ m_scale = src.m_scale;
+ m_rotation = src.m_rotation;
+ m_centerPoint = src.m_centerPoint;
+ m_alpha = src.m_alpha;
+ m_visible = src.m_visible;
+ m_layer = src.m_layer;
+ MouseOptions = src.MouseOptions;
+ }
+
+ // Shared transform surface (declared on both IVisualContainer and ISprite).
+ public Vector3 Position { get => m_position; set => m_position = value; }
+ public Vector2 Size { get => m_size; set => m_size = value; }
+ public Vector3 Scale { get => m_scale; set => m_scale = value; }
+ public AxisAngle Rotation { get => m_rotation; set => m_rotation = value; }
+ public Vector3 CenterPoint { get => m_centerPoint; set => m_centerPoint = value; }
+ public float Alpha { get => m_alpha; set => m_alpha = value; }
+ public bool Visible { get => m_visible; set => m_visible = value; }
+ public uint Layer { get => m_layer; set => m_layer = value; }
+
+ // The "force" overloads exist so callers can bypass change coalescing; our
+ // implementation applies changes immediately, so force is a no-op distinction.
+ public void SetPosition(Vector3 value, bool force) => m_position = value;
+ public void SetSize(Vector2 value, bool force) => m_size = value;
+ public void SetScale(Vector3 value, bool force) => m_scale = value;
+ public void SetRotation(AxisAngle value, bool force) => m_rotation = value;
+ public void SetAlpha(float value, bool force) => m_alpha = value;
+
+ public void AddGradient(IGradient gradient)
+ {
+ if (gradient is GLGradient g)
+ {
+ g.RegisterUsage(this);
+ m_gradients.Add(g);
+ }
+ }
+
+ public void RemoveAllGradients()
+ {
+ foreach (GLGradient g in m_gradients)
+ g.UnregisterUsage(this);
+ m_gradients.Clear();
+ }
+
+ internal IReadOnlyList Gradients => m_gradients;
+
+ ///
+ /// Local model transform: translate to position, rotate/scale about the center
+ /// point. Matches the Iris convention where position/size are in device pixels
+ /// with the y axis pointing down.
+ ///
+ internal Matrix4X4 LocalMatrix
+ {
+ get
+ {
+ Vector3 c = m_centerPoint;
+ Matrix4X4 toCenter = Matrix4X4.CreateTranslation(-c.X, -c.Y, -c.Z);
+ Matrix4X4 scale = Matrix4X4.CreateScale(m_scale.X, m_scale.Y, m_scale.Z);
+ Matrix4X4 rot = Matrix4X4.CreateFromAxisAngle(
+ new Vector3D(m_rotation.Axis.X, m_rotation.Axis.Y, m_rotation.Axis.Z),
+ m_rotation.Angle);
+ Matrix4X4 fromCenter = Matrix4X4.CreateTranslation(c.X, c.Y, c.Z);
+ Matrix4X4 translate = Matrix4X4.CreateTranslation(m_position.X, m_position.Y, m_position.Z);
+ return toCenter * scale * rot * fromCenter * translate;
+ }
+ }
+
+ /// Draw this visual (and its subtree) with the accumulated parent transform.
+ internal abstract void Render(SceneRenderer renderer, Matrix4X4 parentMatrix, float inheritedAlpha);
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs
new file mode 100644
index 0000000..33cef07
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/GLVisualContainer.cs
@@ -0,0 +1,86 @@
+using System.Collections.Generic;
+using Silk.NET.Maths;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// A transform/grouping node in the visual tree. Renders its children in
+ /// ascending order after applying its own transform.
+ ///
+ public sealed class GLVisualContainer : GLVisual, IVisualContainer
+ {
+ private readonly List m_children = new List();
+ private readonly bool m_isRoot;
+
+ public GLVisualContainer(GLRenderSession session, object ownerData, bool isRoot)
+ : base(session, ownerData)
+ {
+ m_isRoot = isRoot;
+ }
+
+ public bool IsRoot => m_isRoot;
+ public int ChildCount => m_children.Count;
+ public ICamera? Camera { get; set; }
+
+ public void AddChild(IVisual vChild, IVisual vSibling, VisualOrder nOrder)
+ {
+ if (vChild is not GLVisual child)
+ return;
+
+ child.ParentContainer?.RemoveChild(child);
+ child.ParentContainer = this;
+
+ int siblingIndex = vSibling is GLVisual s ? m_children.IndexOf(s) : -1;
+ switch (nOrder)
+ {
+ case VisualOrder.First:
+ m_children.Insert(0, child);
+ break;
+ case VisualOrder.Before when siblingIndex >= 0:
+ m_children.Insert(siblingIndex, child);
+ break;
+ case VisualOrder.After when siblingIndex >= 0:
+ m_children.Insert(siblingIndex + 1, child);
+ break;
+ default: // Any, Last, or unresolved sibling
+ m_children.Add(child);
+ break;
+ }
+
+ child.RegisterUsage(this);
+ }
+
+ public void RemoveChild(IVisual vChild)
+ {
+ if (vChild is GLVisual child && m_children.Remove(child))
+ {
+ child.ParentContainer = null;
+ child.UnregisterUsage(this);
+ }
+ }
+
+ public void RemoveAllChildren()
+ {
+ // Snapshot: UnregisterUsage can trigger disposal which mutates state.
+ foreach (GLVisual child in m_children.ToArray())
+ RemoveChild(child);
+ }
+
+ internal override void Render(SceneRenderer renderer, Matrix4X4 parentMatrix, float inheritedAlpha)
+ {
+ if (!Visible)
+ return;
+
+ Matrix4X4 matrix = LocalMatrix * parentMatrix;
+ float alpha = inheritedAlpha * Alpha;
+
+ // Draw children back-to-front by layer. OrderBy is stable, preserving
+ // insertion order within a layer.
+ m_children.Sort((a, b) => a.Layer.CompareTo(b.Layer));
+ foreach (GLVisual child in m_children)
+ child.Render(renderer, matrix, alpha);
+ }
+
+ protected override void DisposeCore() => RemoveAllChildren();
+ }
+}
diff --git a/UIX.RenderApi.OpenGL/Scene/SharedRenderObject.cs b/UIX.RenderApi.OpenGL/Scene/SharedRenderObject.cs
new file mode 100644
index 0000000..5214b40
--- /dev/null
+++ b/UIX.RenderApi.OpenGL/Scene/SharedRenderObject.cs
@@ -0,0 +1,48 @@
+// In-process, OpenGL-backed implementation of the UIX.RenderApi interfaces.
+// See logs/MicrosoftIris/RenderApiOpenGL.md for design notes.
+
+using System.Collections.Generic;
+
+namespace Microsoft.Iris.Render.OpenGL
+{
+ ///
+ /// Common base for render objects that participate in the reference-counted
+ /// lifetime protocol. The original renderer
+ /// tracks a set of "users" per object; releasing the last user tears the object
+ /// down. We reproduce that behaviour with a simple user set.
+ ///
+ public abstract class SharedRenderObject : ISharedRenderObject
+ {
+ private readonly HashSet