-
-
Notifications
You must be signed in to change notification settings - Fork 2
Bitmap Rendering Path
This page documents Gondwana's CPU-backed bitmap rendering path from the engine
cycle, through RenderSurfaceHost, to the platform-specific presentation
adapter.
The bitmap path is used whenever:
surface.Backbuffer.IsGlThreadRendered == falseThe standard implementation is BitmapBackbuffer. Unlike the GL path, bitmap
rendering is driven directly by the engine thread and uses dirty-region
tracking to avoid redrawing unchanged scene content.
The bitmap path separates rendering from presentation:
- The engine thread updates drawing state.
- The engine thread renders changed scene regions into the bitmap backbuffer.
- The engine takes an
SKImagesnapshot of the backbuffer. - Presentation is posted to the UI thread.
- The platform adapter displays the snapshot.
flowchart TD
A["Engine thread"] --> B["Update state"]
B --> C["Render dirty regions"]
C --> D["Snapshot backbuffer"]
D --> E["Post presentation"]
E --> F["UI thread"]
F --> G["Platform adapter displays image"]
This arrangement is possible because the bitmap backbuffer is CPU-accessible and does not require an OpenGL context to be current while scene content is drawn.
Engine.Cycle() obtains the current high-resolution tick, performs background
work, and uses EngineConfiguration.TargetFPS to determine whether foreground
rendering is due.
flowchart TD
A["Engine.Cycle()"] --> B["EngineDispatcher.Drain()"]
B --> C["Get current tick and delta"]
C --> D["DoBackgroundTasks(tick)"]
D --> E{"Foreground interval elapsed?"}
E -- No --> F["Skip foreground rendering"]
E -- Yes --> G["InvokePreFrameRender"]
G --> H["DoForegroundTasks(tick)"]
H --> I["InvokePostFrameRender"]
Background work runs before rendering and includes input polling, animation, sprite movement, collision resolution, and camera updates. These operations may invalidate scene regions or request a full scene refresh.
The bitmap route is called directly from DoForegroundTasks:
foreach (var surface in RenderSurfaceHostRegistry.All)
{
if (!surface.Backbuffer.IsGlThreadRendered)
surface.RenderToBackbuffer(tick);
}After all bitmap hosts have rendered, the engine makes a second pass to present their backbuffers:
foreach (var surface in RenderSurfaceHostRegistry.All)
{
if (!surface.Backbuffer.IsGlThreadRendered)
surface.PresentBackbufferToAdapter();
}The complete foreground activity is:
flowchart TD
A["DoForegroundTasks(tick)"] --> B["BeforeFrameRender event"]
B --> C["DirectDrawingManager.UpdateAll(tick)"]
C --> D["Enumerate registered hosts"]
D --> E{"GL-thread rendered?"}
E -- Yes --> F["Skip bitmap render call"]
E -- No --> G["RenderToBackbuffer(tick)"]
G --> H{"More hosts?"}
F --> H
H -- Yes --> D
H -- No --> I["Enumerate hosts for presentation"]
I --> J{"GL-thread rendered?"}
J -- Yes --> K["Skip bitmap presentation"]
J -- No --> L["PresentBackbufferToAdapter()"]
L --> M{"More hosts?"}
K --> M
M -- Yes --> I
M -- No --> N["Update gamepad state"]
N --> O["AfterFrameRender event"]
O --> P["Raise PostCycle timers"]
RenderToBackbuffer is the common host-level dispatcher. It raises the
begin/end events and selects the implementation appropriate for the
backbuffer:
RenderBackbufferBegin?.Invoke();
if (Backbuffer.IsGlThreadRendered)
RenderToBackbufferGpuFull(tick);
else
RenderToBackbufferBitmap(tick);
RenderBackbufferEnd?.Invoke();For a bitmap backbuffer, control passes to RenderToBackbufferBitmap.
The bitmap implementation uses the scene's layer RefreshQueue instances to
determine which world regions need to be redrawn.
flowchart TD
A["RenderToBackbufferBitmap(tick)"] --> B{"Any Views?"}
B -- No --> C["Clear entire backbuffer"]
C --> D["Clear FullRefreshNeeded"]
D --> Z["Return"]
B -- Yes --> E{"Visible SceneLayers?"}
E -- No --> F["Clear entire backbuffer"]
E -- Yes --> G{"FullRefreshNeeded?"}
G -- Yes --> H["EnqueueFullSceneRefresh()"]
G -- No --> I{"Scene.IsDirty?"}
I -- No --> J["RenderBackbufferNoOp event"]
J --> Z
I -- Yes --> K["Render Views"]
H --> K
F --> K
K --> L["Clear consumed RefreshQueues"]
L --> M["Clear FullRefreshNeeded"]
M --> N["InvokePostSceneCanvasHooks()"]
N --> Z
When Scene.FullRefreshNeeded is true,
EnqueueFullSceneRefresh():
- Iterates every configured
View. - Gets the view's screen viewport.
- Converts that viewport to a layer-specific world rectangle.
- Expands the world rectangle by one tile in every direction.
- Pixel-aligns the result.
- Adds it to the layer's
RefreshQueue.
The one-tile expansion protects against fractional camera or parallax movement and rounding at tile boundaries.
Each view is rendered independently:
flowchart TD
A["Begin View"] --> B["RenderContext.Push(view, tick)"]
B --> C["Get view overlays"]
C --> D["ForceRefresh overlays"]
D --> E["CollectDirtyScreenArea(view)"]
E --> F["Clip to View viewport"]
F --> G["Exclude higher-Z View overlaps"]
G --> H["PreclearScreenAreas()"]
H --> I["RenderLayerDirtyRegions()"]
I --> J["Draw view overlays"]
J --> K["Restore canvas"]
K --> L["RenderContext.Pop()"]
RenderContext.Push makes the current view and tick available to drawing code.
The finally block guarantees the matching RenderContext.Pop, even if a
drawable throws while rendering.
CollectDirtyScreenArea(view) examines every visible scene layer:
- Skip a layer whose
RefreshQueueis clean. - Snapshot its dirty world rectangles.
- Convert each world rectangle to view screen coordinates.
- Intersect it with the view viewport.
- Discard empty results.
- Add the resulting rectangle without duplicates.
World rectangles are stored per layer because different layers may have different coordinate transforms or parallax behavior. Conversion to screen space therefore occurs separately for each view.
PreclearScreenAreas clears each changed screen patch to
Backbuffer.ClearColor. Clearing may expose content from other visible layers,
so EnqueueForOverlappingSceneLayers converts the cleared screen patch back
into the world coordinates of every visible layer and marks the overlapping
layer regions for rendering.
This prevents a changed or removed foreground drawable from leaving stale pixels or erasing unchanged background content.
RenderLayerDirtyRegions(view, layer):
- Skips a clean layer.
- Iterates the layer's dirty world rectangles.
- Calls
SceneLayer.GetDrawablesInWorldRect. - Projects the world rectangle to a screen rectangle.
- Calls
Backbuffer.DrawDrawables, clipped to that screen rectangle.
DrawDrawables draws visible tiles, sprites, and scene-layer direct drawings.
It also updates the backbuffer's aggregate DirtyRectangle so the presentation
stage knows which adapter pixels changed.
After every view is processed, the visible layers' refresh queues are cleared.
InvokePostSceneCanvasHooks() runs after scene content and overlays have been
drawn but before the frame is finalized. It provides the backbuffer canvas to:
-
RenderBackbufferPostScenesubscribers; and - registered engine plugins through
InvokePostRenderCanvas.
For a bitmap backbuffer, the entire backbuffer is marked dirty before these hooks execute. Arbitrary subscriber or plugin drawing therefore cannot be accidentally omitted from presentation.
PresentBackbufferToAdapter() finalizes the rendered frame:
flowchart TD
A["PresentBackbufferToAdapter()"] --> B{"Adapter assigned?"}
B -- No --> Z["Return"]
B -- Yes --> C["Backbuffer.EndFrame()"]
C --> D{"RedrawDirtyRectangleOnly?"}
D -- Yes --> E["PresentBackbufferRect()"]
D -- No --> F["PresentBackbufferAll()"]
E --> G["Create SKImage snapshot"]
F --> G
G --> H["UiDispatcher.Post(adapter.Present)"]
H --> I["Clear DirtyRectangle"]
I --> J["Backbuffer.BeginFrame()"]
When dirty-only presentation is enabled, the source and destination rectangles identify the changed screen patch. Otherwise, the complete backbuffer snapshot is presented.
Presentation is posted rather than executed on the engine thread because UI framework controls must be accessed from their owning UI thread.
WinFormBitmapRenderSurfaceAdapter.Present stores the newest snapshot and calls
SKControl.Invalidate(). During SKControl.PaintSurface, the adapter:
- Intersects the requested source rectangle with the image.
- Clips the destination to the control bounds.
- Clears the destination patch.
- Draws the corresponding image patch.
- Disposes superseded snapshots after painting.
AvaloniaBitmapRenderSurfaceAdapter.Present stores the snapshot and posts
BlitAndInvalidate at Avalonia's render priority. It copies the image pixels
into a WriteableBitmap, assigns that bitmap to the control, and calls
InvalidateVisual.
This is a CPU pixel-copy presentation path.
BlazorBitmapRenderSurfaceAdapter.Present reads the requested image region into
an RGBA byte array and queues the frame on the Blazor component. The component
then updates the browser canvas.
This path necessarily crosses from Skia's bitmap representation into a browser-compatible pixel buffer.
| Operation | Thread |
|---|---|
| Background updates and camera updates | Engine thread |
DirectDrawingManager.UpdateAll |
Engine thread |
RenderToBackbufferBitmap |
Engine thread |
| Dirty-region collection | Engine thread |
| Bitmap backbuffer drawing | Engine thread |
| Backbuffer snapshot creation | Engine thread |
Adapter Present
|
UI thread |
| Platform paint or visual update | UI thread |
Engine.Cycle()
└─ DoForegroundTasks(tick)
├─ DirectDrawingManager.UpdateAll(tick)
├─ RenderSurfaceHost.RenderToBackbuffer(tick)
│ └─ RenderToBackbufferBitmap(tick)
│ ├─ EnqueueFullSceneRefresh()
│ ├─ CollectDirtyScreenArea(view)
│ ├─ PreclearScreenAreas(view, dirtyRects)
│ │ └─ EnqueueForOverlappingSceneLayers(...)
│ ├─ RenderLayerDirtyRegions(view, layer)
│ │ ├─ SceneLayer.GetDrawablesInWorldRect(...)
│ │ └─ Backbuffer.DrawDrawables(...)
│ └─ InvokePostSceneCanvasHooks()
└─ RenderSurfaceHost.PresentBackbufferToAdapter()
├─ Backbuffer.EndFrame()
├─ Backbuffer.Snapshot()
└─ UiDispatcher.Post(adapter.Present)
└─ Platform-specific UI presentation
The bitmap path is an engine-driven, dirty-region renderer:
- rendering happens directly on the engine thread;
- scene-layer refresh queues identify changed world regions;
- dirty regions are projected independently for each view;
- the bitmap backbuffer tracks the aggregate changed screen area; and
- platform presentation is marshalled onto the UI thread.
It trades bookkeeping complexity for reduced CPU drawing and reduced presentation work when only a small portion of the frame changes.
- Home
- Make Your First Game in 30 Minutes
- Engine Architecture Overview
- Gondwana Engine Lifecycle
- Gondwana CLI Cheatsheet
- Assets Files
- Tilesheets
- Scenes and SceneLayers
- Sprites
- Views, Cameras, and Viewports
- DirectDrawing
- Game State Files
- Logging
- Movement and Controllers
- Input Handling
- Collision Detection
- Timers and Engine Timing
- Using the Effects System
- Engine Configuration