Skip to content

Make RenderContextProvider an abstract class that reports its colour target size - #485

Merged
stanoddly merged 2 commits into
mainfrom
render-context-provider-size
Sep 10, 2026
Merged

Make RenderContextProvider an abstract class that reports its colour target size#485
stanoddly merged 2 commits into
mainfrom
render-context-provider-size

Conversation

@botoddly

@botoddly botoddly commented Sep 10, 2026

Copy link
Copy Markdown
Collaborator

#481 moved the Pixely.Ui build into the update phase. That phase has no render context, so it cannot read ColorTarget.Size the way Render used to. It fell back to the window's size, which is right only when the context targets the swapchain. A context drawing into a same-format target of a different size was left laid out for the window, and the renderer then refused to draw it, so the UI stayed blank.

An earlier revision of #481 patched this with a viewportSource delegate on UseUi. That was dropped: it asked the caller to restate a size the framework already knows, and it could be got wrong as silently as the bug it patched.

The size belongs to whoever decides the target, and that is the provider.

The provider answers for its target

IRenderContextProvider<T> becomes the abstract class RenderContextProvider<T> with a new virtual member:

public virtual ShortSize GetColorTargetSize(Window window)
{
    ArgumentNullException.ThrowIfNull(window);
    return window.RenderSizeInPixels;
}

UseUi resolves the provider it already knows the type of and lays out against that. Nothing changes for a swapchain target, because the default is the window. A provider drawing somewhere else overrides one method, in the class it already had to write.

Named for what it measures. IRenderContext.ColorTarget and Window.ColorTargetFormat make "colour target" the existing vocabulary.

Abstract class rather than an interface

A default interface member would have kept source compatibility, but it is the worse tool here:

  • A default interface member is not callable through the concrete type. myProvider.GetColorTargetSize(window) would not compile without a cast to the interface.
  • Tooling does not offer it. "Implement interface" does not stub a default member, so an implementer never sees it. Discoverability is the whole reason to put the size on the provider, and override autocomplete gives it.

The cost is the type's single inheritance slot. That is unlikely to matter for a provider, and docs/class-registration.md:43 already documents resolving a service by a base class, so the container supports this directly.

CLAUDE.md says composition over inheritance. This is a deliberate exception: a one level extension point with a sensible default is what abstract classes are for, in the shape of TextWriter or HttpMessageHandler.

Breaking

IRenderContextProvider<T> is gone. Implementations derive from RenderContextProvider<T> and mark TryCreateRenderContext as override. Registrations name the class instead of the interface:

builder.AddAlias<RenderContextProvider<GameRenderContext>, GameRenderContextProvider>();

Three implementations in the repository were updated, plus the sample in docs/window-rendering.md.

UiUpdateSystem takes what it uses

#481 gave the system Func<Vector2Int> and Func<bool> rather than a window, because RenderSizeInPixels and IsVisible are non-virtual SDL calls and a system holding a window could not be built in a test. There was never a production case for either delegate. Both always closed over the same window.

A virtual GetColorTargetSize removes that reason for the size, so the system now takes the window and the provider:

internal UiUpdateSystem(UiRoot root, Window window, RenderContextProvider<TRenderContext> contextProvider, int updateOrder)

Window.IsVisible becomes virtual, which is what lets a test answer it without a display. The class is generic over the context type, as UiRenderer<TRenderContext> already is.

The delegates cost more than they looked. Two Funcs say nothing about where their values come from, and codex made the point during the #481 review: a test could not tell which window a closure had captured without calling it or reflecting over compiler generated fields. ResolveUpdateTargets exists to work around that opacity.

Tests

ResolveUpdateTargets is now generic over the context type and returns the provider alongside the root and window.

A case #481 could not reach is now covered directly: that the size the tree is laid out against comes from the provider rather than the window. It needed a GPU before, because UseUi builds UiRenderer eagerly. The system is now built in the test from a fake provider and an uninitialised window that has no SDL size to give, so a viewport taken from the window could not produce the expected answer.

The hidden window test no longer counts on a delegate never being called. It asserts on a provider that records how many times it was asked, which is the behaviour the code claims: a hidden window does not pay for a size call.

The ordering test asserts a sequence rather than build counts. Both systems build once, so counts cannot distinguish them. Verified it fails when the expected sequence is reversed.

@stanoddly
stanoddly merged commit 09e7028 into main Sep 10, 2026
1 check passed
@stanoddly
stanoddly deleted the render-context-provider-size branch September 10, 2026 06:37
@github-actions

Copy link
Copy Markdown

✅ Development package Pixely 0.0.35 published successfully.

Workflow run (attempt 1)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants