Application host
SharpProspero.Application is the layer that turns a module into a running program: a base class that owns the display, controller and frame loop, and a few plain-logic helpers for structuring the code that runs inside it. Everything on this page lives in SharpProspero.Application.
On this page
The application host
ProsperoApp is the base class for a module. Derive from it, override OnFrame, and call Run from the module entry point. The base opens the display and the controller, drives a loop paced to the vertical blank, and tears everything down on exit.
using SharpProspero.Application;
using SharpProspero.Graphics;
using SharpProspero.Interop.Pad;
internal sealed class Game : ProsperoApp
{
protected override void OnFrame(FrameContext context)
{
Surface surface = context.Surface;
surface.Clear(Color.FromRgb(0x10, 0x14, 0x1A));
surface.DrawTextCentered("My Game", 480, 6, Color.White);
if (context.Input.IsPressed(ScePadButton.Options))
context.RequestExit();
}
}
internal static class Program
{
private static void Main()
{
using var app = new Game();
app.Run();
}
}
Three overridable methods bracket the run. Only OnFrame is required.
| Method | Called | Use it for |
|---|---|---|
OnLoad |
once, after the display opens, before the first frame | load resources, build the interface, seed state |
OnFrame |
once per frame | read input, update state, draw into the framebuffer |
OnUnload |
once, after the loop ends, before teardown | release what OnLoad acquired |
Inside the class a few members are available while running: Display returns the open DisplayDevice, GamePad returns the controller (or null when none opened), and Dispatcher is the hand-off point a worker thread uses to apply a result back on the frame thread. Config exposes the settings the app started with.
OnUnloadruns even when a frame throws. The exception still propagates to the caller andDisposereleases the display and controller, but your own cleanup is not skipped on the way out.
Startup settings
Pass an AppConfig to the constructor to change how the host starts; omit it for the defaults. The settings are read once, when Run opens the display and controller.
using SharpProspero.Application;
using SharpProspero.Interop.VideoOut;
var app = new Game(new AppConfig
{
Width = 1920,
Height = 1080,
BufferCount = 2,
FlipMode = VideoOutFlipMode.VSync,
HideSplashScreen = true,
OpenGamePad = true,
});
app.Run();
| Setting | Default | Meaning |
|---|---|---|
Width, Height |
1920 x 1080 |
framebuffer size, from the sizes the output accepts (below) |
BufferCount |
2 |
framebuffers in the swap chain |
UserId |
SceUser.System |
the user the controller opens for; the display is not opened for a user |
FlipMode |
VideoOutFlipMode.VSync |
flip timing used each frame |
HideSplashScreen |
true |
remove the boot splash before the first frame |
OpenGamePad |
true |
open a controller for UserId at startup |
The output takes 1920x1080, 3840x2160, 720x480 and 720x576, or a width that is a multiple of 32 from 1280 to 1888 with a height nine sixteenths of it. Any other size makes Run throw ArgumentOutOfRangeException before the display opens. Of those sizes only 1920x1080 is accepted on every console; the rest need the console set up for them and are otherwise refused when the buffers are registered.
When OpenGamePad is set but no controller is present, the host runs anyway and GamePad is null; FrameContext.Input reports the resting sample so per-frame code needs no special case.
The host opens the display in the layout the output scans out, and no setting changes that. FrameContext.Surface is therefore a row-major buffer of its own, and presenting a frame walks every pixel to move what was drawn into the scan-out buffer. Budget that pass on top of your own drawing: at 1920x1080 it moves just over two million pixels each frame. Drawing through the graphics processor writes into the scan-out buffer directly and has no pass to pay, but that path drives the display and its flips itself rather than through this host - see GPU command layer.
The frame context
OnFrame receives a FrameContext. One instance is reused across every frame, so the loop allocates nothing steady-state — read its fields, but do not hold a reference past the current frame.
| Member | What it carries |
|---|---|
Surface |
the framebuffer to draw this frame into |
FrameIndex |
zero-based frame counter since Run began |
DeltaSeconds |
seconds between the start of this frame and the start of the previous one, read from the monotonic counter |
TotalSeconds |
seconds since Run began |
Input |
the latest controller sample |
PreviousInput |
the sample from the previous frame |
Dispatcher |
the hand-off point drained once per frame before OnFrame |
DeltaSeconds is the value to scale movement and animation by, so behavior stays the same whatever the frame rate. For a smooth clock, a cooldown or a fixed-step accumulator, feed it to the helpers in Timing.
The context also compares this frame’s input against the last one, so button-edge handling needs no bookkeeping of your own:
protected override void OnFrame(FrameContext context)
{
if (context.Held(ScePadButton.Cross))
Charge(context.DeltaSeconds); // every frame the button is down
if (context.Pressed(ScePadButton.Circle))
Fire(); // the frame it goes down
if (context.Released(ScePadButton.Circle))
Release(); // the frame it comes up
if (context.Pressed(ScePadButton.Options))
context.RequestExit();
}
RequestExit sets a flag the loop checks after the frame is presented; the current frame still finishes and flips. ExitRequested reports whether it has been called.
The frame lifecycle
Run opens the devices, calls OnLoad, then loops until a frame requests exit, and finally unwinds through OnUnload and Dispose. Each iteration measures the frame time, refreshes the input sample, drains any work handed back from a worker thread, calls OnFrame, and presents the framebuffer.
flowchart TD
A[Run] --> B[Open display and controller]
B --> C[OnLoad]
C --> D{Frame loop}
D --> E[Measure delta and total time]
E --> F[Read controller into Input]
F --> G[Dispatcher: run pending work]
G --> H[OnFrame draws the Surface]
H --> I[Present the framebuffer]
I --> J{ExitRequested?}
J -->|no| D
J -->|yes| K[OnUnload]
K --> L[Dispose: release controller and display]
Because the loop reuses one context and draws into framebuffers allocated up front, a running application does not grow the heap each frame. Keep per-frame code free of allocation to hold that property; Memory covers the heap ceiling and how to watch it.
Structuring an app
Three helpers organize the code that runs inside the loop. None of them touch a device — they are plain logic you drive from OnLoad and OnFrame.
States
StateMachine<TState> runs a program as a set of named states, one active at a time, each with optional work on entry, every frame, and exit — a menu, a level, a pause screen, a results page. Configure the states, Start in one, Update each frame, and TransitionTo another when something happens. The enter and exit callbacks stay paired, so a state always cleans up after itself.
using SharpProspero.Application;
StateMachine<Screen> game = new();
game.Configure(Screen.Menu, onUpdate: dt => { if (start) game.TransitionTo(Screen.Play); })
.Configure(Screen.Play, onEnter: LoadLevel, onUpdate: Step, onExit: UnloadLevel);
game.Start(Screen.Menu);
// each frame:
game.Update(context.DeltaSeconds);
Transitioning to the active state does nothing; a Transitioned event reports the state left and the state entered. Current returns the active state and IsRunning whether Start has been called.
Call
TransitionTofrom a per-frame callback, not from an enter, exit orTransitionedcallback. Starting a transition from inside one would leave the machine half-moved, so it is refused with an exception.
Undo and redo
CommandStack gives an editor-style history. Run a change through Execute — either an ICommand or a do/undo pair of delegates — and it is performed and remembered; Undo and Redo walk the history, and a new change after an undo discards the redo branch.
var history = new CommandStack();
history.Execute(() => item.Rename("new"), () => item.Rename("old"));
history.Undo(); // back to "old"
history.Redo(); // "new" again
The delegate pair is wrapped in a DelegateCommand; write your own ICommand when the do and undo need to share state. CanUndo, CanRedo, UndoCount and RedoCount drive interface elements, and Limit caps how many steps are kept — the oldest drop off past it.
For a run of small changes that should read as one step — typing characters, dragging a slider — implement ICoalescingCommand. When the command on top of the history absorbs the next one, the two collapse into a single undo.
sealed class SetText : ICoalescingCommand
{
private readonly TextBox _box;
private readonly string _before;
private string _after;
public SetText(TextBox box, string before, string after)
{
_box = box;
_before = before;
_after = after;
}
public void Do() => _box.Text = _after;
public void Undo() => _box.Text = _before;
public bool TryCoalesceWith(ICommand next)
{
if (next is not SetText typed)
return false;
_after = typed._after; // fold the newer keystroke into this step
return true;
}
}
Events
EventHub lets parts of an application talk without holding references to each other. A subscriber asks for a message type; a publisher sends one and every subscriber for that type receives it, synchronously, in subscription order. The message type is the channel, so unrelated messages stay separate.
var events = new EventHub();
using IDisposable subscription = events.Subscribe<ScoreChanged>(m => hud.SetScore(m.Total));
events.Publish(new ScoreChanged(1200));
// a record makes a tidy message
public sealed record ScoreChanged(int Total);
Dispose the token from Subscribe to stop receiving; a handler may subscribe or unsubscribe while a message is being delivered without disturbing the one in flight. An exception a handler throws propagates to the publisher and stops the remaining handlers for that message, so keep handlers from throwing on the normal path. SubscriberCount<T>() reports how many handlers are registered for one message type, and Clear drops every subscription at once — useful when a screen tears down and its handlers go with it.
What the process is
ProcessInfo in SharpProspero.Application answers the plain questions about the running process:
its identifier, the size of a memory page, how many descriptors it may hold open at once, and the
arguments it was started with.
using SharpProspero.Application;
int ceiling = ProcessInfo.MaximumOpenDescriptors; // how many files or sockets can be open at once
string[] args = ProcessInfo.Arguments(); // the first is the module's own path
Guid id = ProcessInfo.NewIdentifier(); // drawn by the system, unique across processes
Read MaximumOpenDescriptors before a build decides how many files or sockets to keep open rather
than fixing a number in the source. NewIdentifier is the route to naming a save slot, a capture or a
session so two runs cannot collide.
Where to go next
- Timing — turn
DeltaSecondsinto clocks, cooldowns and a fixed timestep. - Threading — run work off the frame thread and hand results back through the
Dispatcher. - Diagnostics — logging and frame-time statistics for a running loop.
- Input — the full controller, keyboard and mouse surface behind
FrameContext.Input. - Interface toolkit — build screens from controls instead of drawing them by hand.