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 m_users = new HashSet(); + private bool m_disposed; + + public int UsageCount => m_users.Count; + + public void RegisterUsage(object user) + { + if (user != null) + m_users.Add(user); + } + + public void UnregisterUsage(object user) + { + if (user != null && m_users.Remove(user) && m_users.Count == 0) + Dispose(); + } + + protected bool IsDisposed => m_disposed; + + protected void Dispose() + { + if (m_disposed) + return; + m_disposed = true; + DisposeCore(); + } + + /// Release any GPU/native resources. Runs at most once. + protected virtual void DisposeCore() + { + } + } +} diff --git a/UIX.RenderApi.OpenGL/Sound/GLSoundDevice.cs b/UIX.RenderApi.OpenGL/Sound/GLSoundDevice.cs new file mode 100644 index 0000000..83e602b --- /dev/null +++ b/UIX.RenderApi.OpenGL/Sound/GLSoundDevice.cs @@ -0,0 +1,34 @@ +namespace Microsoft.Iris.Render.OpenGL +{ + /// + /// Silent sound device. Volume/mute state is tracked so callers behave, but no audio + /// is produced. Stage-3 TODO: back this with Silk.NET.OpenAL for cross-platform audio. + /// + public sealed class GLSoundDevice : ISoundDevice + { + public GLSoundDevice(SoundDeviceType deviceType) => DeviceType = deviceType; + + public SoundDeviceType DeviceType { get; } + public bool Mute { get; set; } + public float Volume { get; set; } = 1f; + + public ISoundBuffer CreateSoundBuffer(object objUser, ISoundData soundData) + => new GLSoundBuffer(soundData); + } + + public sealed class GLSoundBuffer : SharedRenderObject, ISoundBuffer + { + private readonly ISoundData m_data; + + public GLSoundBuffer(ISoundData data) => m_data = data; + + public ISound CreateSound(object objUser) => new GLSound(); + } + + public sealed class GLSound : SharedRenderObject, ISound + { + // TODO(stage 3): drive an OpenAL source. No-op keeps the UI sound calls safe. + public void Play() { } + public void Stop() { } + } +} diff --git a/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj new file mode 100644 index 0000000..63eb933 --- /dev/null +++ b/UIX.RenderApi.OpenGL/UIX.RenderApi.OpenGL.csproj @@ -0,0 +1,45 @@ + + + + Library + UIX.RenderApi.OpenGL + Microsoft.Iris.Render.OpenGL + true + true + enable + disable + + + net8.0 + net8.0;net48 + + + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/logs/UIX.RenderApi.OpenGL/Implementation.md b/logs/UIX.RenderApi.OpenGL/Implementation.md new file mode 100644 index 0000000..88396b8 --- /dev/null +++ b/logs/UIX.RenderApi.OpenGL/Implementation.md @@ -0,0 +1,86 @@ +# UIX.RenderApi.OpenGL + +Reverse-chronological log (prepend new entries; never edit older ones). + +## 2026-07-25 — Initial in-process OpenGL renderer + +### Goal +Stage-3 enhancement: an in-process, OpenGL-backed implementation of the +`Microsoft.Iris.Render` interfaces defined in `UIX.RenderApi`, as an alternative +to the native/messaging (`Splash`) engine. New project only; `UIX.RenderApi` is +referenced, not modified. Targets net8.0 and net48. Uses Silk.NET (Windowing, +OpenGL, Input, Maths). PolySharp polyfills for the netfx target. + +### Layout +`UIX.RenderApi.OpenGL/` (namespace `Microsoft.Iris.Render.OpenGL`): +- `OpenGLRenderApi` — public factory (mirrors `RenderApi.CreateEngine`, which we + cannot reuse because it hard-codes the native `RenderEngine` and must not change). +- `Engine/` — `GLRenderEngine` (owns the Silk window + GL context, pumps events, + renders each frame), `GLRenderSession` (object factory), `GLGraphicsDevice`, + `GLRenderWindow`, `GLDisplayManager`/`GLDisplay`, `GLInputSystem`, + `GLHwndHostWindow`. +- `Scene/` — `SharedRenderObject` (ISharedRenderObject usage counting), `GLVisual` + (transform base), `GLVisualContainer`, `GLSprite`, `GLImage` (GL texture), + `GLGradient`, `GLCamera`, `GLEffect`/`GLEffectTemplate`, `GLVideoStream`. +- `Animation/` — `GLAnimationSystem`, `GLAnimation`/`GLAnimationGroup`, + `GLKeyframeAnimation`, `GLExternalAnimationInput`/`GLAnimationInputProvider`. +- `Sound/` — silent `GLSoundDevice`/`GLSoundBuffer`/`GLSound`. +- `Rendering/SceneRenderer` — GL shader program + quad drawing. + +### Rendering approach (real logic) +Orthographic, top-left origin, pixel-space projection (`CreateOrthographicOffCenter(0,w,h,0,-1,1)`), +alpha blending, depth test off, draw order = tree order then by `Layer`. Sprites +draw as quads; content from the sprite's effect's first image (a lazily-uploaded +GL texture, BGRA), otherwise the sprite's DebugColor. Visual transform = +translate(-center) · scale · rotate(axis-angle) · translate(center) · translate(position), +accumulated down the tree with inherited alpha. + +Matrix note: Silk.NET.Maths matrices are row-major; we upload with `transpose=false` +so GLSL reads the transpose and the shader uses column-vector order +`uProj * uModel * v`, which reproduces the row-vector composite. See +`SceneRenderer.UploadMatrix`. + +### Documented stubs / TODOs (stage 3 follow-ups) +- Sound is silent (TODO: Silk.NET.OpenAL). +- Effects are typed property bags; no shader compilation. 9-slice, coord maps and + gradient alpha ramps are recorded but not composited. +- Animations run the play-state machine and fire events but do not yet interpolate + target properties. +- Video streams carry metadata only. +- `GLHwndHostWindow` tracks state only (native HWND embedding has no portable GL form). +- Back-buffer capture signals completion without writing a file. +- `GraphicsDeviceType` has no OpenGL member; we report `Direct3D9` as the + hardware-accelerated stand-in (the UI branches on GDI-vs-accelerated). + +### Multi-targeting decision +Task asked for net8.0 + net48. In this repo net48 only builds on Windows (the +referenced projects, incl. UIX.RenderApi, only emit their netfx output there — see +MicrosoftIris/Directory.Build.props and ZuneDBApi/CLAUDE.md "on Linux only net8.0 +TFMs build"). So the csproj declares `net8.0` always and adds `net48` only under +`IsOsPlatform('Windows')`, mirroring the projects it depends on. PolySharp + +Silk.NET (netstandard2.0) cover the netfx target. + +### Blocker observed (not caused by this project) +`UIX.RenderApi` does not currently build for net8.0 in the working tree: the +`MicrosoftIris` submodule is checked out on branch `exp/native-libs-impl` at +`651dcab "Empty UIXrender and UIXsup, retry impl"`, which emptied `UIXrender`. +`UIX.RenderApi/Protocol/EngineApi.cs` (non-netfx path) needs +`Microsoft.Iris.Render.Engine`/`.Interop` from `UIXrender`, so net8.0 compilation +fails until UIXrender is reimplemented. The superproject-recorded submodule commit +`7d96b4f` also fails to build UIXrender (missing Win32/HANDLE interop). `UIX.RenderApi` +itself is byte-identical between those two commits. + +Because of this, the new project cannot be compiled through its `ProjectReference` +in the current tree. It was instead validated by compiling all of its sources +against the prebuilt `UIX.RenderApi.dll` +(`ZuneImpl/bin/x64/Debug/net8.0/UIX.RenderApi.dll`, 2026-07-22) plus the Silk.NET +packages — result: build succeeded (only CS0067 "unused event" warnings for the +not-yet-wired window/animation events). The user's submodule checkout was left +untouched (a temporary checkout of `7d96b4f` for inspection was reverted back to +`exp/native-libs-impl`). + +Open question for the human: once UIXrender's managed engine is restored so +`UIX.RenderApi` builds for net8.0, this project should build via its ProjectReference +with no changes. Should `UIX.RenderApi.OpenGL` also be added to a solution +(ZuneUIXTools.sln)? Left out for now to avoid editing the submodule's tracked +solution during its WIP.