diff --git a/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs
new file mode 100644
index 00000000..a6d5d67a
--- /dev/null
+++ b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs
@@ -0,0 +1,443 @@
+using System.Windows;
+
+using Vortice;
+using Vortice.Direct3D11;
+using Vortice.DirectComposition;
+using Vortice.DXGI;
+using Vortice.Mathematics;
+
+using FlyleafLib.MediaFramework.MediaFrame;
+
+using static FlyleafLib.Utils.NativeMethods;
+
+using ID3D11Texture2D = Vortice.Direct3D11.ID3D11Texture2D;
+
+using FlyleafLib;
+
+namespace FlyleafLib.MediaFramework.MediaRenderer;
+
+///
+/// Child renderer that shares the D3D11 device with the main renderer and presents frames to its own swap chain
+/// Supports individual pan, zoom, and rotation settings without affecting the main renderer
+///
+public class ChildRenderer : IDisposable
+{
+ #region Properties
+ public int UniqueId { get; private set; }
+ public int ControlWidth { get; private set; }
+ public int ControlHeight { get; private set; }
+ public bool Disposed { get; private set; } = true;
+ public bool IsActive { get; private set; }
+
+ public Viewport GetViewport { get; private set; }
+ public event EventHandler ViewportChanged;
+
+ // Pan/Zoom/Rotation support (independent from main renderer)
+ public int PanXOffset { get => panXOffset; set => SetPanX(value); }
+ int panXOffset;
+
+ public int PanYOffset { get => panYOffset; set => SetPanY(value); }
+ int panYOffset;
+
+ public double Zoom { get => zoom; set => SetZoom(value); }
+ double zoom = 1;
+
+ public Point ZoomCenter { get => zoomCenter; set => SetZoomCenter(value); }
+ Point zoomCenter = new(0.5, 0.5);
+
+ public uint Rotation { get => rotation; set => UpdateRotation(value); }
+ uint rotation;
+
+ public bool HFlip { get => hFlip; set { hFlip = value; UpdateRotation(rotation); } }
+ bool hFlip;
+
+ public bool VFlip { get => vFlip; set { vFlip = value; UpdateRotation(rotation); } }
+ bool vFlip;
+
+ public CornerRadius CornerRadius { get => cornerRadius; set => UpdateCornerRadius(value); }
+ CornerRadius cornerRadius = new(0);
+ #endregion
+
+ #region Internal Fields
+ internal nint ControlHandle;
+ internal Renderer ParentRenderer;
+
+ IDXGISwapChain1 swapChain;
+ ID3D11Texture2D backBuffer;
+ ID3D11RenderTargetView backBufferRtv;
+ IDCompositionDevice dCompDevice;
+ IDCompositionVisual dCompVisual;
+ IDCompositionTarget dCompTarget;
+
+ volatile bool canRenderPresent;
+ bool needsResize;
+ bool needsViewport;
+
+ object lockRender = new();
+
+ // For viewport calculation
+ uint visibleWidth;
+ uint visibleHeight;
+ double curRatio, keepRatio, fillRatio;
+ int sideXPixels;
+ int sideYPixels;
+ uint textWidth, textHeight;
+ CropRect cropRect;
+ VideoProcessorRotation d3d11vpRotation = VideoProcessorRotation.Identity;
+ #endregion
+
+ public ChildRenderer(Renderer parentRenderer, nint handle, int uniqueId = -1)
+ {
+ ParentRenderer = parentRenderer;
+ ControlHandle = handle;
+ UniqueId = uniqueId == -1 ? GetUniqueId() : uniqueId;
+ }
+
+ static int curUniqueId;
+ static int GetUniqueId() => curUniqueId++;
+
+ public void Initialize()
+ {
+ lock (lockRender)
+ {
+ if (!Disposed)
+ Dispose();
+
+ Disposed = false;
+
+ RECT rect = new();
+ GetWindowRect(ControlHandle, ref rect);
+ ControlWidth = rect.Right - rect.Left;
+ ControlHeight = rect.Bottom - rect.Top;
+
+ try
+ {
+ var swapChainDesc = GetSwapChainDesc(2, 2);
+ swapChain = Engine.Video.Factory.CreateSwapChainForComposition(ParentRenderer.Device, swapChainDesc);
+
+ DComp.DCompositionCreateDevice(ParentRenderer.dxgiDevice, out dCompDevice).CheckError();
+ dCompDevice.CreateTargetForHwnd(ControlHandle, false, out dCompTarget).CheckError();
+ dCompDevice.CreateVisual(out dCompVisual).CheckError();
+ dCompVisual.SetContent(swapChain).CheckError();
+ dCompTarget.SetRoot(dCompVisual).CheckError();
+ dCompDevice.Commit().CheckError();
+
+ backBuffer = swapChain.GetBuffer(0);
+ backBufferRtv = ParentRenderer.Device.CreateRenderTargetView(backBuffer);
+
+ Engine.Video.Factory.MakeWindowAssociation(ControlHandle, WindowAssociationFlags.IgnoreAll);
+
+ IsActive = true;
+ canRenderPresent = true;
+ needsResize = true;
+ }
+ catch (Exception e)
+ {
+ ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] Initialization failed: {e.Message}");
+ Dispose();
+ throw;
+ }
+ }
+ }
+
+ private SwapChainDescription1 GetSwapChainDesc(int width, int height)
+ => new()
+ {
+ BufferUsage = Usage.RenderTargetOutput,
+ Format = ParentRenderer.Config.Video.Swap10Bit ? Format.R10G10B10A2_UNorm : (ParentRenderer.Config.Video.SwapForceR8G8B8A8 ? Format.R8G8B8A8_UNorm : Format.B8G8R8A8_UNorm),
+ Width = (uint)width,
+ Height = (uint)height,
+ AlphaMode = AlphaMode.Premultiplied,
+ SwapEffect = SwapEffect.FlipDiscard,
+ Scaling = Scaling.Stretch,
+ BufferCount = Math.Max(ParentRenderer.Config.Video.SwapBuffers, 2),
+ SampleDescription = new SampleDescription(1, 0),
+ Flags = SwapChainFlags.None
+ };
+
+ public void Resize(int width, int height)
+ {
+ lock (lockRender)
+ {
+ if (Disposed || !IsActive)
+ return;
+
+ if (width == ControlWidth && height == ControlHeight)
+ return;
+
+ ControlWidth = width;
+ ControlHeight = height;
+ needsResize = true;
+ canRenderPresent = width > 0 && height > 0;
+ }
+ }
+
+ private void ResizeBuffersInternal()
+ {
+ if (!needsResize)
+ return;
+
+ needsResize = false;
+
+ if (ControlWidth <= 0 || ControlHeight <= 0)
+ {
+ canRenderPresent = false;
+ return;
+ }
+
+ backBufferRtv?.Dispose();
+ backBuffer?.Dispose();
+
+ swapChain.ResizeBuffers(0, (uint)ControlWidth, (uint)ControlHeight, Format.Unknown, SwapChainFlags.None).CheckError();
+
+ backBuffer = swapChain.GetBuffer(0);
+ backBufferRtv = ParentRenderer.Device.CreateRenderTargetView(backBuffer);
+
+ canRenderPresent = true;
+ needsViewport = true;
+ }
+
+ #region Pan/Zoom/Rotation Methods
+ public void SetPanX(int panX, bool refresh = true)
+ {
+ panXOffset = panX;
+ SetViewport(refresh);
+ }
+
+ public void SetPanY(int panY, bool refresh = true)
+ {
+ panYOffset = panY;
+ SetViewport(refresh);
+ }
+
+ public void SetZoom(double zoom, bool refresh = true)
+ {
+ this.zoom = zoom;
+ SetViewport(refresh);
+ }
+
+ public void SetZoomCenter(Point p, bool refresh = true)
+ {
+ zoomCenter = p;
+ SetViewport(refresh);
+ }
+
+ public void SetZoomAndCenter(double zoom, Point p, bool refresh = true)
+ {
+ this.zoom = zoom;
+ zoomCenter = p;
+ SetViewport(refresh);
+ }
+
+ public void SetPanAll(int panX, int panY, uint rotation, double zoom, Point p, bool refresh = true)
+ {
+ panXOffset = panX;
+ panYOffset = panY;
+ this.zoom = zoom;
+ zoomCenter = p;
+ UpdateRotation(rotation, false);
+ SetViewport(refresh);
+ }
+
+ private void UpdateRotation(uint angle, bool refresh = true)
+ {
+ rotation = angle;
+
+ // Map rotation to D3D11 VideoProcessor rotation
+ d3d11vpRotation = (rotation, hFlip, vFlip) switch
+ {
+ (0, false, false) => VideoProcessorRotation.Identity,
+ (90, false, false) => VideoProcessorRotation.Rotation90,
+ (180, false, false) => VideoProcessorRotation.Rotation180,
+ (270, false, false) => VideoProcessorRotation.Rotation270,
+ _ => VideoProcessorRotation.Identity
+ };
+
+ if (refresh)
+ SetViewport();
+ }
+
+ private void UpdateCornerRadius(CornerRadius value)
+ {
+ cornerRadius = value;
+ // Corner radius implementation would go here
+ }
+
+ public void SetViewport(bool refresh = true)
+ {
+ lock (lockRender)
+ {
+ if (Disposed || !IsActive)
+ return;
+
+ needsViewport = true;
+ }
+ }
+
+ private void SetViewportInternal()
+ {
+ if (!needsViewport)
+ return;
+
+ needsViewport = false;
+
+ // Get video dimensions from parent renderer
+ visibleWidth = ParentRenderer.VisibleWidth;
+ visibleHeight = ParentRenderer.VisibleHeight;
+
+ if (visibleWidth == 0 || visibleHeight == 0)
+ {
+ GetViewport = new Viewport(0, 0, ControlWidth, ControlHeight);
+ ViewportChanged?.Invoke(this, EventArgs.Empty);
+ return;
+ }
+
+ // Calculate aspect ratios
+ curRatio = (double)visibleWidth / visibleHeight;
+ keepRatio = (double)ControlWidth / ControlHeight;
+ fillRatio = keepRatio;
+
+ int x, y, newWidth, newHeight, xZoomPixels, yZoomPixels;
+
+ if (curRatio < fillRatio)
+ {
+ newHeight = ControlHeight;
+ newWidth = (int)(visibleWidth * ControlHeight / visibleHeight);
+ sideXPixels = ControlWidth - newWidth;
+ sideYPixels = 0;
+ }
+ else
+ {
+ newWidth = ControlWidth;
+ newHeight = (int)(visibleHeight * ControlWidth / visibleWidth);
+ sideXPixels = 0;
+ sideYPixels = ControlHeight - newHeight;
+ }
+
+ // Apply zoom
+ xZoomPixels = (int)((newWidth * zoom) - newWidth);
+ yZoomPixels = (int)((newHeight * zoom) - newHeight);
+
+ newWidth += xZoomPixels;
+ newHeight += yZoomPixels;
+
+ // Apply zoom center
+ x = (int)((sideXPixels / 2) - (xZoomPixels * zoomCenter.X));
+ y = (int)((sideYPixels / 2) - (yZoomPixels * zoomCenter.Y));
+
+ // Apply pan offsets
+ x += panXOffset;
+ y += panYOffset;
+
+ GetViewport = new Viewport(x, y, newWidth, newHeight);
+ ViewportChanged?.Invoke(this, EventArgs.Empty);
+ }
+ #endregion
+
+ ///
+ /// Renders the current frame from the parent renderer to this child's swap chain
+ /// Should be called by the parent renderer during its render loop
+ ///
+ internal void RenderFrame(VideoFrame frame, bool secondField)
+ {
+ lock (lockRender)
+ {
+ if (Disposed || !IsActive || !canRenderPresent)
+ return;
+
+ try
+ {
+ ResizeBuffersInternal();
+ SetViewportInternal();
+
+ if (!canRenderPresent)
+ return;
+
+ var context = ParentRenderer.context;
+
+ // Set our render target
+ context.OMSetRenderTargets(backBufferRtv);
+ context.ClearRenderTargetView(backBufferRtv, ParentRenderer.Config.Video._BackgroundColor);
+
+ // Set our viewport
+ context.RSSetViewport(GetViewport);
+
+ // If frame uses Flyleaf shaders (has shader resource views)
+ if (frame.srvs != null)
+ {
+ // Use shader resource views directly - this is the efficient path
+ context.PSSetShaderResources(0, frame.srvs);
+ context.Draw(6, 0);
+ }
+ else
+ {
+ // For D3D11 Video Processor output, we need the parent to render to backBuffer first
+ // Then we copy from parent's backBuffer to ours
+ // This is less efficient but necessary for D3D11VP path
+ // The parent renderer will already have rendered to its backBuffer
+ // We just need to copy it with our viewport
+
+ // Note: The parent's backBuffer should already have the rendered frame
+ // We'll create an SRV from parent's backBuffer and render it to ours
+ using var parentBackBufferSrv = ParentRenderer.Device.CreateShaderResourceView(ParentRenderer.backBuffer);
+ context.PSSetShaderResources(0, new[] { parentBackBufferSrv });
+ context.Draw(6, 0);
+ }
+
+ // Restore parent's viewport
+ context.RSSetViewport(ParentRenderer.GetViewport);
+ }
+ catch (Exception e)
+ {
+ ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] RenderFrame failed: {e.Message}");
+ }
+ }
+ }
+
+ ///
+ /// Presents the rendered frame to the screen
+ /// Should be called by the parent renderer after all child renderers have rendered
+ ///
+ internal void Present()
+ {
+ lock (lockRender)
+ {
+ if (Disposed || !IsActive || !canRenderPresent)
+ return;
+
+ try
+ {
+ swapChain.Present(ParentRenderer.Config.Video.VSync, PresentFlags.None).CheckError();
+ }
+ catch (Exception e)
+ {
+ ParentRenderer.Log?.Error($"[ChildRenderer #{UniqueId}] Present failed: {e.Message}");
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ lock (lockRender)
+ {
+ if (Disposed)
+ return;
+
+ Disposed = true;
+ IsActive = false;
+
+ backBufferRtv?.Dispose();
+ backBuffer?.Dispose();
+ swapChain?.Dispose();
+
+ if (dCompDevice != null)
+ {
+ dCompDevice.Dispose();
+ dCompDevice = null;
+ }
+
+ dCompVisual?.Dispose();
+ dCompTarget?.Dispose();
+ }
+ }
+}
diff --git a/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md
new file mode 100644
index 00000000..c463fab0
--- /dev/null
+++ b/FlyleafLib/MediaFramework/MediaRenderer/ChildRenderers.README.md
@@ -0,0 +1,199 @@
+# Child Renderers Support
+
+This feature allows displaying the same video stream in multiple host controls simultaneously, useful for scenarios like:
+- Displaying multiple video streams as thumbnails with a selected thumbnail shown in a larger view
+- Picture-in-picture implementations
+- Multi-monitor setups with synchronized playback
+
+## Architecture
+
+The implementation uses **child swap chains** that share the same D3D11 device with the main renderer. This approach:
+- Avoids copying frames between devices (efficient)
+- Requires all surfaces to be on the same GPU adapter
+- Supports independent pan, zoom, and rotation on each child renderer
+- Child renderers are not full players (no separate audio output)
+
+## Key Components
+
+### 1. ChildRenderer Class
+Located in `FlyleafLib/MediaFramework/MediaRenderer/ChildRenderer.cs`
+
+A child renderer manages its own swap chain and can render frames from the parent renderer with independent viewing settings:
+- Pan (X/Y offsets)
+- Zoom (scale factor and center point)
+- Rotation (0°, 90°, 180°, 270°)
+- Horizontal/Vertical flip
+
+### 2. Renderer.ChildRenderers.cs
+Extension to the main Renderer class that provides:
+- `RegisterChildRenderer()` - Registers a child renderer
+- `UnregisterChildRenderer()` - Unregisters a child renderer
+- `GetChildRenderers()` - Gets all registered child renderers
+- Internal methods for rendering and presenting to child renderers
+
+### 3. Player.ChildRenderers.cs
+Player-level API for easier child renderer management:
+- `CreateChildRenderer()` - Creates and registers a new child renderer
+- `RemoveChildRenderer()` - Removes and disposes a child renderer
+- `GetChildRenderers()` - Gets all child renderers
+
+## Usage Example (WPF)
+
+### Basic Setup
+
+```csharp
+using FlyleafLib;
+using FlyleafLib.MediaPlayer;
+using FlyleafLib.MediaFramework.MediaRenderer;
+
+public partial class MainWindow : Window
+{
+ private Player player;
+ private ChildRenderer thumbnailRenderer;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+
+ // Initialize player
+ Engine.Start(new EngineConfig());
+ player = new Player(new Config());
+
+ // Attach player to main view
+ mainVideoHost.Player = player;
+
+ // Create child renderer for thumbnail view
+ // thumbnailHost is another FlyleafHost control
+ var thumbnailHandle = new WindowInteropHelper(thumbnailHost.Surface).Handle;
+ thumbnailRenderer = player.CreateChildRenderer(thumbnailHandle);
+
+ if (thumbnailRenderer != null)
+ {
+ // Configure thumbnail with zoom out for overview
+ thumbnailRenderer.Zoom = 0.5;
+ }
+ }
+
+ private void OnThumbnailClick(object sender, EventArgs e)
+ {
+ // Can adjust child renderer view independently
+ thumbnailRenderer.Zoom = 1.0;
+ thumbnailRenderer.SetPanAll(0, 0, 0, 1.0, new Point(0.5, 0.5));
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ thumbnailRenderer?.Dispose();
+ player?.Dispose();
+ base.OnClosed(e);
+ }
+}
+```
+
+### Multiple Thumbnails
+
+```csharp
+private List thumbnailRenderers = new();
+
+private void CreateThumbnailGrid()
+{
+ foreach (var thumbnailHost in thumbnailHostsGrid.Children.OfType())
+ {
+ var handle = new WindowInteropHelper(thumbnailHost.Surface).Handle;
+ var childRenderer = player.CreateChildRenderer(handle);
+
+ if (childRenderer != null)
+ {
+ // Configure each thumbnail independently
+ childRenderer.Zoom = 0.3;
+ thumbnailRenderers.Add(childRenderer);
+ }
+ }
+}
+
+private void CleanupThumbnails()
+{
+ foreach (var renderer in thumbnailRenderers)
+ {
+ player.RemoveChildRenderer(renderer);
+ }
+ thumbnailRenderers.Clear();
+}
+```
+
+### Dynamic Pan/Zoom on Child Renderer
+
+```csharp
+// Pan the child renderer view
+thumbnailRenderer.PanXOffset = 100; // Pan right by 100 pixels
+thumbnailRenderer.PanYOffset = -50; // Pan up by 50 pixels
+
+// Zoom with center point
+thumbnailRenderer.SetZoomAndCenter(2.0, new Point(0.3, 0.3));
+
+// Zoom to specific point maintaining that point's position
+Point clickPoint = new Point(200, 150);
+thumbnailRenderer.ZoomWithCenterPoint(clickPoint, 1.5);
+
+// Rotate
+thumbnailRenderer.Rotation = 90; // 0, 90, 180, or 270
+
+// Flip
+thumbnailRenderer.HFlip = true;
+thumbnailRenderer.VFlip = false;
+
+// Reset all transformations
+thumbnailRenderer.SetPanAll(0, 0, 0, 1.0, new Point(0.5, 0.5));
+```
+
+## Performance Considerations
+
+1. **Shared Device**: All child renderers share the same D3D11 device as the main renderer
+ - Must be on the same GPU adapter
+ - Cross-adapter scenarios will require additional copying (not currently implemented)
+
+2. **Video Processor Path**:
+ - For D3D11 Video Processor: Child renderers copy from parent's backBuffer
+ - For Flyleaf Shaders: Child renderers use frame's SRVs directly (more efficient)
+
+3. **Synchronization**: Child renderers render and present synchronously with the main renderer
+ - All rendering happens on the same thread
+ - Present operations are sequential
+
+## Limitations
+
+1. Child renderers are **not independent players**:
+ - No separate audio output
+ - No separate playback controls
+ - No separate subtitle rendering (currently)
+
+2. GPU adapter constraints:
+ - All surfaces must be on the same GPU adapter
+ - Moving windows to different monitors on different GPUs is not supported
+
+3. No separate configuration:
+ - VSync and buffer settings are inherited from parent renderer
+
+## Future Enhancements
+
+Possible future improvements:
+- Support for subtitle rendering on child renderers
+- Support for overlay content on child renderers
+- Cross-adapter rendering with automatic texture copying
+- Asynchronous rendering option for better performance
+- Independent VSync settings per child renderer
+
+## Thread Safety
+
+All child renderer operations are thread-safe:
+- Registration/unregistration is protected by locks
+- Rendering operations are synchronized with the main renderer
+- Disposal is safe to call from any thread
+
+## Best Practices
+
+1. **Initialize after player is ready**: Create child renderers after the player has opened a video stream
+2. **Dispose properly**: Always dispose child renderers before disposing the player
+3. **Handle window resize**: Call `childRenderer.Resize(width, height)` when the window size changes
+4. **Check IsActive**: Verify `childRenderer.IsActive` before performing operations
+5. **Handle failures**: Check for null returns from `CreateChildRenderer()` and handle errors gracefully
diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs
new file mode 100644
index 00000000..f226c1f6
--- /dev/null
+++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.ChildRenderers.cs
@@ -0,0 +1,142 @@
+using FlyleafLib.MediaFramework.MediaFrame;
+using FlyleafLib.Utils;
+
+namespace FlyleafLib.MediaFramework.MediaRenderer;
+
+public unsafe partial class Renderer
+{
+ #region Child Renderers Management
+ private readonly List childRenderers = new();
+ private readonly object lockChildRenderers = new();
+
+ ///
+ /// Registers a child renderer that will receive frames from this main renderer
+ /// The child renderer will share the same D3D11 device
+ ///
+ /// The child renderer to register
+ public void RegisterChildRenderer(ChildRenderer childRenderer)
+ {
+ if (childRenderer == null)
+ throw new ArgumentNullException(nameof(childRenderer));
+
+ if (childRenderer.ParentRenderer != this)
+ throw new InvalidOperationException("Child renderer must have this renderer as its parent");
+
+ lock (lockChildRenderers)
+ {
+ if (!childRenderers.Contains(childRenderer))
+ {
+ childRenderers.Add(childRenderer);
+ if (Logger.CanInfo) Log.Info($"Registered child renderer #{childRenderer.UniqueId}");
+ }
+ }
+ }
+
+ ///
+ /// Unregisters a child renderer
+ ///
+ /// The child renderer to unregister
+ public void UnregisterChildRenderer(ChildRenderer childRenderer)
+ {
+ if (childRenderer == null)
+ return;
+
+ lock (lockChildRenderers)
+ {
+ if (childRenderers.Remove(childRenderer))
+ {
+ if (Logger.CanInfo) Log.Info($"Unregistered child renderer #{childRenderer.UniqueId}");
+ }
+ }
+ }
+
+ ///
+ /// Gets all registered child renderers
+ ///
+ public IReadOnlyList GetChildRenderers()
+ {
+ lock (lockChildRenderers)
+ {
+ return childRenderers.ToList();
+ }
+ }
+
+ ///
+ /// Renders the current frame to all active child renderers
+ /// Should be called during the main render loop after rendering to the main swap chain
+ ///
+ private void RenderToChildRenderers(VideoFrame frame, bool secondField)
+ {
+ lock (lockChildRenderers)
+ {
+ if (childRenderers.Count == 0)
+ return;
+
+ foreach (var child in childRenderers)
+ {
+ if (child.IsActive && !child.Disposed)
+ {
+ try
+ {
+ child.RenderFrame(frame, secondField);
+ }
+ catch (Exception e)
+ {
+ Log.Error($"Failed to render to child renderer #{child.UniqueId}: {e.Message}");
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Presents all child renderers
+ /// Should be called after the main renderer presents
+ ///
+ private void PresentChildRenderers()
+ {
+ lock (lockChildRenderers)
+ {
+ if (childRenderers.Count == 0)
+ return;
+
+ foreach (var child in childRenderers)
+ {
+ if (child.IsActive && !child.Disposed)
+ {
+ try
+ {
+ child.Present();
+ }
+ catch (Exception e)
+ {
+ Log.Error($"Failed to present child renderer #{child.UniqueId}: {e.Message}");
+ }
+ }
+ }
+ }
+ }
+
+ ///
+ /// Disposes all child renderers
+ ///
+ private void DisposeChildRenderers()
+ {
+ lock (lockChildRenderers)
+ {
+ foreach (var child in childRenderers)
+ {
+ try
+ {
+ child.Dispose();
+ }
+ catch (Exception e)
+ {
+ Log.Error($"Failed to dispose child renderer #{child.UniqueId}: {e.Message}");
+ }
+ }
+ childRenderers.Clear();
+ }
+ }
+ #endregion
+}
diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs
index 123e97ab..a2425802 100644
--- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs
+++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Device.cs
@@ -67,14 +67,14 @@ static Renderer()
}
internal ID3D11Device Device;
- IDXGIDevice1 dxgiDevice;
+ internal IDXGIDevice1 dxgiDevice;
public FeatureLevel FeatureLevel { get; private set; }
public GPUAdapter GPUAdapter => gpuAdapter;
IDXGIAdapter dxgiAdapter;
GPUAdapter gpuAdapter;
bool gpuForceWarp;
- ID3D11DeviceContext context;
+ internal ID3D11DeviceContext context;
ID3D11Buffer vertexBuffer;
ID3D11InputLayout inputLayout;
@@ -270,6 +270,9 @@ public void Dispose()
LastFrame = null;
}
+ // Dispose all child renderers first
+ DisposeChildRenderers();
+
DisposeSwapChain();
if (use2d)
diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs
index 6a0d1dd1..7aad3a1a 100644
--- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs
+++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.Present.cs
@@ -158,8 +158,13 @@ internal bool PresentPlay()
try
{
if (canRenderPresent)
+ {
swapChain.Present(Config.Video.VSync, PresentFlags.None).CheckError();
+ // Present child renderers
+ PresentChildRenderers();
+ }
+
return true;
}
catch (SharpGenException e)
@@ -207,8 +212,13 @@ internal bool RefreshPlay(bool secondField)
}
if (canRenderPresent)
+ {
swapChain.Present(Config.Video.VSync, PresentFlags.None).CheckError();
+ // Present child renderers
+ PresentChildRenderers();
+ }
+
return true;
}
catch { return false; }
@@ -275,6 +285,9 @@ void RenderFrame(VideoFrame frame, bool secondField) // From Play or Idle (No lo
context.OMSetBlendState(null);
context.RSSetViewport(GetViewport);
}
+
+ // Render to child renderers (they share the same device and context)
+ RenderToChildRenderers(frame, secondField);
}
public FrameStatistics GetFrameStatistics()
diff --git a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs
index 5ba874dc..f718fd03 100644
--- a/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs
+++ b/FlyleafLib/MediaFramework/MediaRenderer/Renderer.SwapChain.cs
@@ -16,7 +16,7 @@ unsafe public partial class Renderer
{
volatile bool canRenderPresent; // Don't render / present during minimize (or invalid size)
- ID3D11Texture2D backBuffer;
+ internal ID3D11Texture2D backBuffer;
ID3D11RenderTargetView backBufferRtv;
IDXGISwapChain1 swapChain;
IDCompositionDevice dCompDevice;
diff --git a/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs b/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs
new file mode 100644
index 00000000..a4a8b4ad
--- /dev/null
+++ b/FlyleafLib/MediaPlayer/Player.ChildRenderers.cs
@@ -0,0 +1,75 @@
+using FlyleafLib.MediaFramework.MediaRenderer;
+
+namespace FlyleafLib.MediaPlayer;
+
+public unsafe partial class Player
+{
+ #region Child Renderers
+ ///
+ /// Creates a child renderer that shares the D3D11 device with the main renderer
+ /// The child renderer will display the same video content with independent pan/zoom/rotation settings
+ ///
+ /// Window handle for the child renderer's output surface
+ /// Optional unique identifier for the child renderer
+ /// The created ChildRenderer instance, or null if no renderer is available
+ public ChildRenderer CreateChildRenderer(nint handle, int uniqueId = -1)
+ {
+ if (renderer == null)
+ {
+ Log?.Error("Cannot create child renderer: Main renderer is not initialized");
+ return null;
+ }
+
+ if (handle == nint.Zero)
+ {
+ Log?.Error("Cannot create child renderer: Invalid window handle");
+ return null;
+ }
+
+ try
+ {
+ var childRenderer = new ChildRenderer(renderer, handle, uniqueId);
+ childRenderer.Initialize();
+ renderer.RegisterChildRenderer(childRenderer);
+
+ Log?.Info($"Created child renderer #{childRenderer.UniqueId}");
+ return childRenderer;
+ }
+ catch (Exception e)
+ {
+ Log?.Error($"Failed to create child renderer: {e.Message}");
+ return null;
+ }
+ }
+
+ ///
+ /// Removes and disposes a child renderer
+ ///
+ /// The child renderer to remove
+ public void RemoveChildRenderer(ChildRenderer childRenderer)
+ {
+ if (childRenderer == null || renderer == null)
+ return;
+
+ try
+ {
+ renderer.UnregisterChildRenderer(childRenderer);
+ childRenderer.Dispose();
+ Log?.Info($"Removed child renderer #{childRenderer.UniqueId}");
+ }
+ catch (Exception e)
+ {
+ Log?.Error($"Failed to remove child renderer: {e.Message}");
+ }
+ }
+
+ ///
+ /// Gets all registered child renderers
+ ///
+ /// Read-only list of child renderers
+ public IReadOnlyList GetChildRenderers()
+ {
+ return renderer?.GetChildRenderers() ?? new List();
+ }
+ #endregion
+}
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml
new file mode 100644
index 00000000..cee9e27d
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml
@@ -0,0 +1,8 @@
+
+
+
+
+
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs
new file mode 100644
index 00000000..0fce44da
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/App.xaml.cs
@@ -0,0 +1,8 @@
+using System.Windows;
+
+namespace FlyleafPlayer.ChildRendererSample
+{
+ public partial class App : Application
+ {
+ }
+}
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj b/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj
new file mode 100644
index 00000000..d85f8b4e
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/FlyleafPlayer (Child Renderers) (WPF).csproj
@@ -0,0 +1,26 @@
+
+
+
+ WinExe
+ net8.0-windows
+ FlyleafPlayer.ChildRendererSample
+ true
+ FlyleafPlayer.ChildRenderers
+ AnyCPU;x86;x64
+ SuRGeoNix © 2025
+ SuRGeoNix
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md b/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md
new file mode 100644
index 00000000..17a2ed37
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/HOW_TO_RUN.md
@@ -0,0 +1,119 @@
+# How to Build and Run Without Visual Studio
+
+## Prerequisites
+
+1. **Windows 10/11** (required - this uses Windows-specific APIs)
+2. **.NET 8 SDK or later**
+ Download from: https://dotnet.microsoft.com/download
+
+## Quick Start
+
+### 1. Install .NET SDK (if not installed)
+```powershell
+# Download and install from:
+https://dotnet.microsoft.com/download/dotnet/8.0
+
+# Verify installation
+dotnet --version
+```
+
+### 2. Clone/Copy the Repository to Windows
+Transfer this entire `/workspaces/Flyleaf` folder to your Windows machine.
+
+### 3. Build the Project
+Open PowerShell or Command Prompt in the Flyleaf directory:
+
+```powershell
+# Navigate to the solution directory
+cd path\to\Flyleaf
+
+# Restore NuGet packages
+dotnet restore FlyleafLib.sln
+
+# Build the solution
+dotnet build FlyleafLib.sln --configuration Release
+```
+
+### 4. Run the Child Renderers Sample
+```powershell
+# Navigate to the sample directory
+cd "Samples\FlyleafPlayer (Child Renderers) (WPF)"
+
+# Run the application
+dotnet run
+```
+
+That's it! The application should launch.
+
+## Alternative: Build Only the Sample
+
+If the full solution has issues, build just the sample:
+
+```powershell
+cd "Samples\FlyleafPlayer (Child Renderers) (WPF)"
+dotnet build
+dotnet run
+```
+
+## Alternative: Using Visual Studio Code
+
+If you have VS Code on Windows:
+
+1. Install VS Code: https://code.visualstudio.com/
+2. Install C# extension: https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.csharp
+3. Open the Flyleaf folder in VS Code
+4. Press `F5` to run
+
+## Alternative: Using Rider
+
+JetBrains Rider is another option:
+1. Install Rider: https://www.jetbrains.com/rider/
+2. Open `FlyleafLib.sln`
+3. Click Run
+
+## Troubleshooting
+
+### "SDK not found"
+Install .NET SDK from: https://dotnet.microsoft.com/download
+
+### "Project targets .NET 10"
+The sample I created uses .NET 8. If you see errors, check the .csproj file has:
+```xml
+net8.0-windows
+```
+
+### "Cannot find FFmpeg"
+The application will try to load FFmpeg binaries. Make sure they exist in the `FFmpeg` folder.
+
+### "Build errors"
+Try:
+```powershell
+dotnet clean
+dotnet restore
+dotnet build
+```
+
+## What You'll See
+
+When running successfully:
+- Main window with video player
+- Two thumbnail views on the right
+- Zoom, rotate, flip controls
+- Status bar at bottom
+
+Open a video file (MP4, MKV, etc.) using the "Open Video" button to see all three views synchronized!
+
+## Running from Executable
+
+After building, you can run the .exe directly:
+```
+Samples\FlyleafPlayer (Child Renderers) (WPF)\bin\Release\net8.0-windows\FlyleafPlayer.ChildRenderers.exe
+```
+
+## Still Having Issues?
+
+Check:
+1. You're on Windows (Linux/Mac won't work)
+2. .NET 8 SDK is installed
+3. You have a display (not running headless)
+4. The project built successfully (check for error messages)
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml
new file mode 100644
index 00000000..811a3f6e
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml
@@ -0,0 +1,157 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ • Open a video file using the Open button
+ • Click thumbnails to toggle zoom
+ • Use control buttons to manipulate views
+ • Each thumbnail is independently controlled
+ • All views share the same video player
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml.cs b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml.cs
new file mode 100644
index 00000000..20c6ceae
--- /dev/null
+++ b/Samples/FlyleafPlayer (Child Renderers) (WPF)/MainWindow.xaml.cs
@@ -0,0 +1,252 @@
+using System.Windows;
+using System.Windows.Input;
+using System.Windows.Interop;
+using FlyleafLib;
+using FlyleafLib.MediaPlayer;
+using FlyleafLib.MediaFramework.MediaRenderer;
+using FlyleafLib.Controls.WPF;
+
+namespace FlyleafPlayer.ChildRendererSample
+{
+ ///
+ /// Sample application demonstrating Child Renderer feature
+ /// Shows the same video in a main view and multiple thumbnail views
+ ///
+ public partial class MainWindow : Window
+ {
+ private Player player;
+ private ChildRenderer thumbnailRenderer1;
+ private ChildRenderer thumbnailRenderer2;
+
+ public MainWindow()
+ {
+ InitializeComponent();
+ InitializePlayer();
+ SetupEventHandlers();
+ }
+
+ private void InitializePlayer()
+ {
+ // Initialize Flyleaf Engine
+ Engine.Start(new EngineConfig()
+ {
+ FFmpegPath = ":FFmpeg",
+ FFmpegDevices = false,
+ PluginsPath = ":Plugins",
+ UIRefresh = false,
+ UIRefreshInterval = 250
+ });
+
+ // Create player configuration
+ var config = new Config
+ {
+ Player =
+ {
+ AutoPlay = true,
+ SeekAccurate = true
+ },
+ Video =
+ {
+ AspectRatio = AspectRatio.Keep,
+ ClearScreen = true
+ },
+ Audio =
+ {
+ Enabled = true
+ },
+ Subtitles =
+ {
+ Enabled = true
+ }
+ };
+
+ // Create player
+ player = new Player(config);
+
+ // Attach to main host
+ MainVideoHost.Player = player;
+
+ // Subscribe to player events
+ player.OpenCompleted += Player_OpenCompleted;
+ }
+
+ private void Player_OpenCompleted(object sender, OpenCompletedArgs e)
+ {
+ if (e.Success)
+ {
+ // Create child renderers after video is opened
+ Dispatcher.Invoke(() =>
+ {
+ CreateChildRenderers();
+ });
+ }
+ }
+
+ private void CreateChildRenderers()
+ {
+ // Create first thumbnail renderer
+ if (Thumbnail1Host.Surface != null)
+ {
+ var handle1 = new WindowInteropHelper(Thumbnail1Host.Surface).Handle;
+ thumbnailRenderer1 = player.CreateChildRenderer(handle1, 1);
+
+ if (thumbnailRenderer1 != null)
+ {
+ // Configure as small thumbnail with slight zoom out
+ thumbnailRenderer1.Zoom = 0.8;
+ UpdateStatus($"Created thumbnail renderer 1 (ID: {thumbnailRenderer1.UniqueId})");
+ }
+ }
+
+ // Create second thumbnail renderer
+ if (Thumbnail2Host.Surface != null)
+ {
+ var handle2 = new WindowInteropHelper(Thumbnail2Host.Surface).Handle;
+ thumbnailRenderer2 = player.CreateChildRenderer(handle2, 2);
+
+ if (thumbnailRenderer2 != null)
+ {
+ // Configure with different view
+ thumbnailRenderer2.Zoom = 1.2;
+ thumbnailRenderer2.PanXOffset = -50;
+ UpdateStatus($"Created thumbnail renderer 2 (ID: {thumbnailRenderer2.UniqueId})");
+ }
+ }
+ }
+
+ private void SetupEventHandlers()
+ {
+ // File menu
+ btnOpen.Click += BtnOpen_Click;
+ btnExit.Click += (s, e) => Close();
+
+ // Thumbnail interactions
+ Thumbnail1Border.MouseLeftButtonDown += (s, e) => OnThumbnailClicked(thumbnailRenderer1, 1);
+ Thumbnail2Border.MouseLeftButtonDown += (s, e) => OnThumbnailClicked(thumbnailRenderer2, 2);
+
+ // Zoom controls
+ btnZoomIn1.Click += (s, e) => ZoomThumbnail(thumbnailRenderer1, 1.2);
+ btnZoomOut1.Click += (s, e) => ZoomThumbnail(thumbnailRenderer1, 0.8);
+ btnReset1.Click += (s, e) => ResetThumbnail(thumbnailRenderer1);
+
+ btnZoomIn2.Click += (s, e) => ZoomThumbnail(thumbnailRenderer2, 1.2);
+ btnZoomOut2.Click += (s, e) => ZoomThumbnail(thumbnailRenderer2, 0.8);
+ btnReset2.Click += (s, e) => ResetThumbnail(thumbnailRenderer2);
+
+ // Rotation controls
+ btnRotate1.Click += (s, e) => RotateThumbnail(thumbnailRenderer1);
+ btnRotate2.Click += (s, e) => RotateThumbnail(thumbnailRenderer2);
+
+ // Flip controls
+ btnFlipH1.Click += (s, e) => FlipThumbnail(thumbnailRenderer1, true, false);
+ btnFlipV1.Click += (s, e) => FlipThumbnail(thumbnailRenderer1, false, true);
+ btnFlipH2.Click += (s, e) => FlipThumbnail(thumbnailRenderer2, true, false);
+ btnFlipV2.Click += (s, e) => FlipThumbnail(thumbnailRenderer2, false, true);
+ }
+
+ private void BtnOpen_Click(object sender, RoutedEventArgs e)
+ {
+ var dialog = new Microsoft.Win32.OpenFileDialog
+ {
+ Filter = "Video Files|*.mp4;*.mkv;*.avi;*.mov;*.wmv;*.flv;*.webm|All Files|*.*",
+ Title = "Select a video file"
+ };
+
+ if (dialog.ShowDialog() == true)
+ {
+ player.Open(dialog.FileName);
+ }
+ }
+
+ private void OnThumbnailClicked(ChildRenderer renderer, int index)
+ {
+ if (renderer == null)
+ return;
+
+ UpdateStatus($"Thumbnail {index} clicked - Toggling zoom");
+
+ // Toggle between normal and zoomed view
+ if (renderer.Zoom > 1.0)
+ renderer.Zoom = 0.8;
+ else
+ renderer.Zoom = 1.5;
+ }
+
+ private void ZoomThumbnail(ChildRenderer renderer, double factor)
+ {
+ if (renderer == null)
+ return;
+
+ renderer.Zoom *= factor;
+ renderer.Zoom = Math.Max(0.1, Math.Min(renderer.Zoom, 5.0)); // Clamp between 0.1 and 5.0
+ UpdateStatus($"Zoom: {renderer.Zoom:F2}x");
+ }
+
+ private void ResetThumbnail(ChildRenderer renderer)
+ {
+ if (renderer == null)
+ return;
+
+ renderer.SetPanAll(0, 0, 0, 1.0, new Point(0.5, 0.5));
+ renderer.HFlip = false;
+ renderer.VFlip = false;
+ UpdateStatus("Reset to default view");
+ }
+
+ private void RotateThumbnail(ChildRenderer renderer)
+ {
+ if (renderer == null)
+ return;
+
+ renderer.Rotation = (renderer.Rotation + 90) % 360;
+ UpdateStatus($"Rotation: {renderer.Rotation}°");
+ }
+
+ private void FlipThumbnail(ChildRenderer renderer, bool horizontal, bool vertical)
+ {
+ if (renderer == null)
+ return;
+
+ if (horizontal)
+ {
+ renderer.HFlip = !renderer.HFlip;
+ UpdateStatus($"Horizontal Flip: {renderer.HFlip}");
+ }
+
+ if (vertical)
+ {
+ renderer.VFlip = !renderer.VFlip;
+ UpdateStatus($"Vertical Flip: {renderer.VFlip}");
+ }
+ }
+
+ private void UpdateStatus(string message)
+ {
+ StatusText.Text = $"{DateTime.Now:HH:mm:ss} - {message}";
+ }
+
+ protected override void OnClosed(EventArgs e)
+ {
+ // Clean up child renderers
+ if (thumbnailRenderer1 != null)
+ {
+ player?.RemoveChildRenderer(thumbnailRenderer1);
+ thumbnailRenderer1 = null;
+ }
+
+ if (thumbnailRenderer2 != null)
+ {
+ player?.RemoveChildRenderer(thumbnailRenderer2);
+ thumbnailRenderer2 = null;
+ }
+
+ // Dispose player
+ player?.Dispose();
+
+ // Shutdown engine
+ Engine.Dispose();
+
+ base.OnClosed(e);
+ }
+ }
+}