Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions samples/Approvals/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@
// The desktop input bridge: it mints the local person and routes pointer-downs
// to the overlay, and wraps the window clipboard behind the transfer seam.
var source = new AppWindowInteractionSource(overlay, new AppWindowClipboard(win));
win.PointerInput += source.OnPointerEvent;
loop.Handler.PointerInput = (_, e) => source.OnPointerEvent(e);

var planner = new Actor("planner", "Planner", ActorKind.Ai, ActorLocality.Local);
var formFiller = new Actor("form-filler", "Form Filler", ActorKind.Automation, ActorLocality.Local);
Expand All @@ -63,7 +63,7 @@ async Task Authorize(Actor actor, InteractionCapability capability, string promp
: $" GRANTED {actor.DisplayName} / {capability}");
}

win.KeyInput += async e =>
loop.Handler.KeyInput = async (_, e) =>
{
if (!e.IsDown)
{
Expand Down
11 changes: 7 additions & 4 deletions samples/HelloWindow/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@
var dirtyFrames = 30;
var presented = 0;

win.Resized += f =>
var handler = new CallbackAppWindowHandler();
win.Handler = handler;

handler.Resized = (_, f) =>
{
gpu.Configure(f.PixelWidth, f.PixelHeight);
dirtyFrames = 30; // reconfigure discards surface contents, so redraw
};

win.PointerInput += e =>
handler.PointerInput = (_, e) =>
{
if (e.Kind != PointerEventKind.Move)
{
Expand All @@ -66,7 +69,7 @@
dirtyFrames = Math.Max(dirtyFrames, 3);
};

win.KeyInput += e =>
handler.KeyInput = (_, e) =>
{
if (!e.IsDown)
{
Expand All @@ -85,7 +88,7 @@
// finishes, otherwise idle frames sleep inside Skyline's loop.
win.IsDirty = () => maxFrames > 0 || animate || dirtyFrames > 0;

win.RenderFrame += f =>
handler.RenderFrame = (_, f) =>
{
if (animate)
{
Expand Down
8 changes: 5 additions & 3 deletions samples/InteractiveCanvas/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
var drawing = false;
var presented = 0;

win.Resized += _ =>
var handler = loop.Handler;

handler.Resized = (_, _) =>
{
renderer.MarkDirty(); // logical size changed — geometry must recompute
loop.RequestRedraw();
};

win.PointerInput += e =>
handler.PointerInput = (_, e) =>
{
if (e.Kind == PointerEventKind.Down)
{
Expand All @@ -54,7 +56,7 @@
}
};

win.KeyInput += e =>
handler.KeyInput = (_, e) =>
{
if (!e.IsDown)
{
Expand Down
2 changes: 1 addition & 1 deletion samples/TexturedQuad/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
var animate = true;
var presented = 0;

win.KeyInput += e =>
loop.Handler.KeyInput = (_, e) =>
{
if (!e.IsDown)
{
Expand Down
2 changes: 1 addition & 1 deletion samples/ThrowingFrame/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@

var rendered = 0;

win.KeyInput += e =>
loop.Handler.KeyInput = (_, e) =>
{
if (e.IsDown && e.Key == Key.Escape)
{
Expand Down
2 changes: 1 addition & 1 deletion samples/TransparentWindow/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@
using var quads = new QuadRenderer(loop.Gpu, loop.Surface.Format);
var slider = new Slider();
slider.Layout(window.CurrentFrame.LogicalWidth, window.CurrentFrame.LogicalHeight);
window.PointerInput += slider.OnPointer;
loop.Handler.PointerInput = (_, e) => slider.OnPointer(e);

Console.WriteLine("Drag the slider to change the window transparency.");

Expand Down
35 changes: 19 additions & 16 deletions samples/TwoWindows/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,25 +72,28 @@ public WindowRenderer(GpuContext gpu, WindowSurface surface, AppWindow window, (
var frame = window.CurrentFrame;
_surface.Configure(frame.PixelWidth, frame.PixelHeight);

window.Resized += f => _surface.Configure(f.PixelWidth, f.PixelHeight);
window.KeyInput += e =>
window.Handler = new CallbackAppWindowHandler
{
if (e.IsDown && e.Key == Skyline.Input.Key.Escape)
Resized = (_, f) => _surface.Configure(f.PixelWidth, f.PixelHeight),
KeyInput = (_, e) =>
{
window.RequestClose();
}
};
window.RenderFrame += f =>
{
if (!RenderFrame(f))
{
return;
}

if (_maxFrames > 0 && _presented >= _maxFrames)
if (e.IsDown && e.Key == Skyline.Input.Key.Escape)
{
window.RequestClose();
}
},
RenderFrame = (_, f) =>
{
window.RequestClose();
}
if (!RenderFrame(f))
{
return;
}

if (_maxFrames > 0 && _presented >= _maxFrames)
{
window.RequestClose();
}
},
};
}

Expand Down
2 changes: 1 addition & 1 deletion samples/UnionApp/App.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public FrameOutcome Run()
});

var presented = 0;
win.KeyInput += e =>
loop.Handler.KeyInput = (_, e) =>
{
if (e.IsDown && e.Key == Key.Escape)
{
Expand Down
35 changes: 29 additions & 6 deletions src/Skyline.Render/FrameLoop.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ namespace Skyline.Render;
/// <summary>
/// Owns the per-frame ritual every windowed wgpu app repeats: pace, acquire
/// (with stale-swapchain recovery), begin a clear pass, run your draw, end,
/// submit, count, present. It also wires the window's resize, idle, and
/// render events. What it owns, it owns completely — but every raw handle
/// submit, count, present. It also drives the window's resize, idle, and
/// render callbacks. What it owns, it owns completely — but every raw handle
/// (<see cref="Gpu"/>, <see cref="Surface"/>, and the live encoder/view/pass
/// on each <see cref="Frame"/>) stays one property away, so an app drops to
/// raw wgpu without leaving the loop.
Expand Down Expand Up @@ -103,6 +103,16 @@ internal static FrameLoop CreateForTest(GpuContext gpu, FramePacer pacer, FrameL
/// <summary>Your per-frame draw. Runs after the clear pass begins (when enabled) and before submit/present. Set it before the loop starts.</summary>
public FrameCallback? OnRender { get; set; }

/// <summary>
/// Callbacks for the window's input, resize, and focus — already created, so
/// set them directly (e.g. <c>loop.Handler.KeyInput = ...</c>). The loop owns
/// the window's <see cref="AppWindow.Handler"/> while attached and forwards
/// everything except the frame draw here. Draw through <see cref="OnRender"/>,
/// not the render callback. Resize forwards after the loop reconfigures the
/// surface.
/// </summary>
public CallbackAppWindowHandler Handler { get; } = new();

/// <summary>The GPU context — raw <c>Api</c>, device, and queue handles for the eject.</summary>
public GpuContext Gpu => _gpu;

Expand Down Expand Up @@ -131,8 +141,22 @@ private void Wire(AppWindow window)
_cancel = _surface.CancelFrame;
var frame = window.CurrentFrame;
_surface.Configure(frame.PixelWidth, frame.PixelHeight);
window.Resized += OnResized;
window.RenderFrame += OnRenderFrame;
// The loop is the window's single handler. It drives the frame itself
// and forwards input, focus, and post-reconfigure resize to the app's
// Handler.
window.Handler = new CallbackAppWindowHandler
{
RenderFrame = (_, info) => OnRenderFrame(info),
Resized = (w, f) =>
{
OnResized(f);
Handler.OnResized(w, f);
},
PointerInput = (w, e) => Handler.OnPointerInput(w, e),
KeyInput = (w, e) => Handler.OnKeyInput(w, e),
TextInput = (w, e) => Handler.OnTextInput(w, e),
FocusChanged = (w, e) => Handler.OnFocusChanged(w, e),
};
// Continuous loops render every frame (IsDirty left null). Event-driven
// loops idle until a redraw is pending. Consuming the flag here — at the
// gate that decides whether to render — makes the check-and-consume one
Expand Down Expand Up @@ -252,8 +276,7 @@ public void Dispose()
_disposed = true;
if (_window is not null)
{
_window.Resized -= OnResized;
_window.RenderFrame -= OnRenderFrame;
_window.Handler = null;
}
// Attach built the context and pacer, so Attach disposes them (and the
// context disposes its surface). Over and the test seam borrow them, so
Expand Down
8 changes: 4 additions & 4 deletions src/Skyline/AppHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ namespace Skyline;
/// own display's rate. Two vsynced windows on different monitors never
/// throttle each other, because no two blocking waits share a thread.
///
/// Threading contract for consumers: input events and <see cref="Invoke"/>
/// actions run on the main thread. <see cref="AppWindow.RenderFrame"/> and
/// <see cref="AppWindow.Resized"/> run on that window's render thread, so
/// all of a window's GPU work stays on one thread.
/// Threading contract for consumers: input callbacks and <see cref="Invoke"/>
/// actions run on the main thread. <see cref="AppWindowHandler.OnRenderFrame"/>
/// and <see cref="AppWindowHandler.OnResized"/> run on that window's render
/// thread, so all of a window's GPU work stays on one thread.
/// </summary>
public sealed class AppHost : IDisposable
{
Expand Down
44 changes: 18 additions & 26 deletions src/Skyline/AppWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public AppWindow(AppWindowOptions? options = null)
if (Host is null)
{
_geom = g;
Resized?.Invoke(Frame(0));
Handler?.OnResized(this, Frame(0));
}
else
{
Expand Down Expand Up @@ -91,23 +91,23 @@ public AppWindow(AppWindowOptions? options = null)
Thread.Sleep(8);
return;
}
RenderFrame?.Invoke(Frame(delta));
Handler?.OnRenderFrame(this, Frame(delta));
};

_backend.Pointer += e => PointerInput?.Invoke(e);
_backend.Key += e => KeyInput?.Invoke(e);
_backend.Text += e => TextInput?.Invoke(e);
_backend.FocusChanged += focused => FocusChanged?.Invoke(focused);
_backend.Pointer += e => Handler?.OnPointerInput(this, e);
_backend.Key += e => Handler?.OnKeyInput(this, e);
_backend.Text += e => Handler?.OnTextInput(this, e);
_backend.FocusChanged += focused => Handler?.OnFocusChanged(this, new WindowFocusEvent(focused));
}

internal void RaisePointer(PointerEventKind kind, float x, float y, int button, float wheelDx, float wheelDy, ModifierKeys modifiers = ModifierKeys.None) =>
PointerInput?.Invoke(new PointerEvent(kind, x, y, button, wheelDx, wheelDy, modifiers));
Handler?.OnPointerInput(this, new PointerEvent(kind, x, y, button, wheelDx, wheelDy, modifiers));

internal void RaiseKey(bool isDown, Silk.NET.Input.Key key, ModifierKeys modifiers = ModifierKeys.None) =>
KeyInput?.Invoke(new KeyEvent(isDown, MapKey(key), (int)key, modifiers));
Handler?.OnKeyInput(this, new KeyEvent(isDown, MapKey(key), (int)key, modifiers));

internal void RaiseText(char character) =>
TextInput?.Invoke(new TextEvent(character));
Handler?.OnTextInput(this, new TextEvent(character));

/// <summary>
/// The native window as a surface source. Hand this to your renderer
Expand All @@ -130,21 +130,13 @@ internal void RaiseText(char character) =>
/// </summary>
public nint? MetalLayer => _backend.SurfaceSource is WindowSurfaceSource.MetalLayer m ? m.Layer : null;

/// <summary>Draw and present a frame. Skyline never presents for you.</summary>
public event Action<FrameInfo>? RenderFrame;

/// <summary>Framebuffer size changed. Reconfigure your swapchain.</summary>
public event Action<FrameInfo>? Resized;

public event Action<PointerEvent>? PointerInput;
public event Action<KeyEvent>? KeyInput;
public event Action<TextEvent>? TextInput;

/// <summary>
/// The window gained or lost focus (true = active). Pause animation,
/// timers, and media while blurred; resume on focus.
/// The window's single handler — input, render, resize, and focus callbacks.
/// A window has one owner, so it takes one handler, not multicast events.
/// Subclass <see cref="AppWindowHandler"/>, or use
/// <see cref="CallbackAppWindowHandler"/> for lambda-style hookup.
/// </summary>
public event Action<bool>? FocusChanged;
public AppWindowHandler? Handler { get; set; }

/// <summary>
/// When set and returning false, the frame is skipped and the loop
Expand Down Expand Up @@ -197,7 +189,7 @@ public int Run()
/// </summary>
public void RequestRedraw() => _redraw.Set();

/// <summary>Resize to a logical size. Main thread only; <see cref="Resized"/> follows.</summary>
/// <summary>Resize to a logical size. Main thread only; <see cref="AppWindowHandler.OnResized"/> follows.</summary>
public void Resize(int width, int height) => _backend.Resize(width, height);

/// <summary>
Expand Down Expand Up @@ -229,7 +221,7 @@ public void Restore()
/// <summary>
/// Process pending OS events once, without the built-in loop. For
/// consumers that drive their own frame loop — engines, benchmarks —
/// instead of subscribing to <see cref="RenderFrame"/>.
/// instead of setting a <see cref="Handler"/> for <see cref="AppWindowHandler.OnRenderFrame"/>.
/// </summary>
public void PumpEvents() => _backend.PumpEventsOnce();

Expand All @@ -250,7 +242,7 @@ private FrameInfo Frame(double delta)
internal bool IsClosing => _backend.IsClosing;
internal bool IsMinimized => _minimized;
internal bool ShouldRenderNow => IsDirty?.Invoke() != false;
internal void RaiseRenderFrame(double delta) => RenderFrame?.Invoke(Frame(delta));
internal void RaiseRenderFrame(double delta) => Handler?.OnRenderFrame(this, Frame(delta));
internal bool WaitForRedraw(int milliseconds) => _redraw.WaitOne(milliseconds);

internal bool TryConsumePendingResize(out FrameInfo frame)
Expand All @@ -266,7 +258,7 @@ internal bool TryConsumePendingResize(out FrameInfo frame)
return true;
}

internal void RaiseResized(FrameInfo frame) => Resized?.Invoke(frame);
internal void RaiseResized(FrameInfo frame) => Handler?.OnResized(this, frame);

internal static Input.Key MapKey(Silk.NET.Input.Key k)
{
Expand Down
Loading