Pipelines, materials, and render submission

Prepare complete draw packets from retained assets and copied game state, append them to an ordered frame, and let the runtime render and recycle that frame. Rendering introduces the interfaces; Ownership and Cleanup explains why packets can retain resources without keeping a closed graphics backend usable.

Task

Path

Draw ordinary sprites, images or text

Default asset materials, selected during upload.

Change one object’s material or shader parameters

Custom sprite material example, sharing assets and material handles between objects.

Load authored shader/material recipes

Shader/material manifests through the asset loader.

Render a toolkit UI

Default UI materials, selected automatically by its uploader.

Customize UI rendering

Custom UI materials, using loaded material handles.

Construct a material directly

Manual material construction, on the graphics owner.

Change filtering for one image binding

Nearest-sampling example, retaining a sampler separately from image pixels.

CPU submission

CE::Rendering::DrawPacket2D retains geometry, an immutable material generation, and copied resolved parameters. Asset submission helpers select sprite/tile ranges, whole graphics, and laid-out font glyphs before rendering, without binding native resources. CE::Rendering::Submission::Context2D inserts the standard image sampler at unit zero by default. Set its image to another ImageParameter2D for a custom key/unit, or to std::nullopt for manual image selection where the asset helper permits it. Text layout is const and returns glyph placements; deprecated BitmapFont bank selection remains typed. Resolved glyph packets retain the geometry/atlas even after the Font object is released. CE::Rendering::PassWriter validates individual packets or a whole glyph group before publication and assigns stable authored order. CE::Rendering::Frame keeps reusable vector capacity and releases packet/pass handles on recycle. The OpenGL renderer checks its native pipeline domain, then calls the validated fixed-state draw path in authored order. Whole-image graphics, tiles, tile-animation values and text use CE::Rendering::Submission CPU helpers. Their asset/playback contract defines metadata and glyph units, direct-construction limits and image-key insertion. The Unicode service provides retained shaped glyph pages through its CE::Text::Rendering::Generation submission overload. Opt-in positioned text runs prepare placements once and submit contiguous atlas-page ranges with a whole-text transform. They preserve order, clipping, sampling and retained generations. General packet merging and reorder-safe sorting remain separate extensions; the renderer still plays authored packets in order.

Frame storage and writers

Frame writers borrow a slot; they do not lock it or publish it across threads. The runtime’s frame boundary owns that transfer. Only active passes are visible through passes(), whose span borrows the slot and is invalidated by outer-vector growth. A pass writer uses an index that survives that growth, but references from parameters()/constraints() do not. Keep every writer within one exclusive preparation phase, before publication or recycling.

Each add() validates before inserting. The group overload validates all packets and reserves capacity before appending any of them. Validation/allocation failure leaves that pass’s earlier draws intact; it does not roll back an entire frame or other application side effects. Writer destruction performs no commit or rollback. Recycle a partially prepared frame before reuse. begin_pass() owns its values and copies matrices; a pass’s constraints are immutable during its preparation through this writer API. Authored pass/draw order is the playback order regardless of the packet’s order_sensitive metadata.

Recycle only after playback or supersession excludes readers, on the resource release owner required by the backend. It releases active packet/pass handles and keeps vector capacity for later preparation. Native render/presentation failures can leave partial output or changed native state; CPU validation is not a transaction over backend execution.

Camera observations

CE::Rendering::CameraBase is unsynchronized mutable CPU state. Setters and matrix references belong to one owner; a revision is a change counter, not an atomic publication mechanism. Copy matrices into a pass before sharing them with playback. Changed framebuffer size, view or perspective advances the revision once, while equal configurations leave it unchanged. View equality is exact per component; view matrices are not checked for finiteness or invertibility.

Framebuffer dimensions are pixels. Negative dimensions throw before changing the camera; zero dimensions are retained while projection clamps each axis to at least one. CE::Rendering::Camera2D uses GLM orthographic bounds [0, width] and [0, height], Y-up, with near/far 0/1. CE::Rendering::Camera3D uses vertical FOV in degrees and framebuffer aspect; finite 0 < FOV < 180 and 0 < near < far are required, with plane distances in view units. Invalid perspective values leave the old configuration unchanged. Neither camera selects renderer depth policy or converts logical window coordinates to framebuffer pixels; that choice belongs to the consumer.

Parameters and ownership

A packet retains the selected resource generations as well as its copied parameter values. Its material reaches the executable through a pipeline:

        flowchart TB
    accTitle: What a draw packet retains
    accDescr: A packet retains geometry, a material and resolved values. The material retains a pipeline and its executable program; image and sampler handles are retained through resolved values and material defaults.
    packet["Draw packet"] -->|retains| geometry["Geometry"]
    packet -->|retains| material["Material"]
    packet -->|owns| values["Resolved values"]
    material -->|retains| pipeline["Pipeline"]
    pipeline -->|retains| program["Executable program"]
    values -->|retain| images["Images and samplers"]
    material -->|defaults retain| images
    

Parameter values own scalars, vectors, matrices, and their map keys. CE::Rendering::Parameters::ImageBinding owns a shared const image handle, a binding-unit request and an optional retained const sampler override. CE::Rendering::Sampling::Sampler owns immutable sampling options and exposes the backend’s effective anisotropy. Each pass and draw supplies copied CE::Rendering::Parameters::Set values; material defaults belong to the immutable material snapshot. Resolution returns a new owned set, never a view into the caller’s inputs.

Source

Ownership

Precedence

Contract default

Pipeline generation

Lowest

Pass custom values

Pass submission

Overrides contract defaults

Material defaults/resources

Material generation

Overrides pass custom values

Draw custom values

Draw submission

Highest

Projection/view/model/alpha/scale semantics come from CE::Rendering::Parameters::PassSemantics and DrawSemantics. The contract explicitly opts into each semantic; an effect does not inherit sprite uniforms automatically. Custom layers cannot override semantic keys. Custom values support float/int/uint/bool, float vec2/vec3/vec4, mat4, and sampler2D. Matrix scope is deliberately limited to the existing mat4 transforms.

Contracts reject duplicate/empty keys, invalid types, duplicate or mistyped engine semantics, and invalid defaults. Each source layer rejects unknown keys, wrong types, semantic overrides, and null images, even if a later value would hide the problem. After all layers resolve, required missing values and colliding sampler units fail. Optional values with no source/default are absent from the result. The native builder requires a reset/default for an active optional custom uniform. An inactive optional uniform produces no upload. Missing active engine semantics reject an incomplete packet. Binding rewrites every active contracted uniform, so an absent optional value cannot inherit a previous draw’s value.

Independent binding

Geometry selects only its vertex array/buffer. Image binding accepts a zero-based unit for each request; OpenGL converts it to GL_TEXTURE0 + unit and checks the current context’s unit limit, sampled once during upload rather than queried on every draw. Image upload uses unit zero as temporary setup and stores no unit. Asset submission uses the standard key/unit unless its context selects another. Two material recipes can retain the same image with different unit requests without mutating each other’s recipe or the cached image.

Immutable sampling

CE::Rendering::Sampling::Options selects nearest/linear minification and magnification, no/nearest/ linear mipmap filtering, clamp/repeat/mirrored-repeat wrapping on each axis and disabled/maximum-supported anisotropy. Engine defaults are linear filtering, linear mipmaps, clamp-to-edge and maximum-supported anisotropy. OpenGL image creation applies those defaults and generates a complete mip chain. Font-atlas creation keeps its separate no-mipmap policy. Unsupported anisotropy resolves to an effective value of one; the uploaded sampler exposes the effective value.

CE::Rendering::Resources::Provider::create_sampler(options) is an optional capability. Its default rejects rather than silently dropping overrides. OpenGL creates immutable sampler objects on the loading owner and weakly reuses live matching options. An CE::Rendering::Parameters::ImageBinding without an override uses the image’s default policy. With an override, the packet retains it independently of image pixels. An FX material’s named sampling policy fills a missing binding sampler; an explicit submitted sampler wins. Ordinary indexed materials need no sampling declaration or image knowledge.

OpenGL validates sampler/image domains, unit bounds and mip completeness before binding. A draw using default image sampling unbinds a preceding sampler object; state from another draw cannot leak into it. Samplers retire through the native resource lifetime just like textures. Shared images and previously published frames remain unchanged when another binding selects different sampling. For exact nearest sampling, disable anisotropy explicitly. Grid UVs still address cell edges; sampling support does not supply atlas padding or prevent every adjacent-cell bleed.

Choose nearest sampling for pixel art

On the provider’s platform/loading owner, with its graphics context current:

namespace Sampling = CE::Rendering::Sampling;
Sampling::Options options;
options.minification = Sampling::Filter::Nearest;
options.magnification = Sampling::Filter::Nearest;
options.mipmaps = Sampling::MipmapFilter::None;
options.anisotropy = Sampling::Anisotropy::Disabled;
auto nearest = provider.create_sampler(options);
style.parameters["image"] = CE::Rendering::Parameters::ImageBinding{image, 0, nearest};

Include <cheryl/assets/resources/sampler.h> and the draw/submission headers. Here image is a retained image from that provider and style.material declares its public sampler parameter as image. When this explicit binding supplies the image, set context.image = std::nullopt to disable automatic insertion; inserting the same draw-layer key twice rejects. The frame retains the sampler with the binding. Other draws using the same pixels can keep their original filtering. Nearest sampling still requires suitable atlas padding and placement to avoid adjacent-cell bleed or unintended boundary pixels.

Frame and recipe integration

OpenGL pipeline builders validate explicit mappings/reflection, required/optional values, copied-value uploads, and sampler domains/units. CE::Rendering::OpenGL::Pipeline::draw() applies complete fixed state after validating pass constraints, geometry, and parameters. Frame packets use that path. Linked attributes match position3 at location zero and UV2 at location one. The colored layout additionally maps float RGBA at location two, with an explicit CE::Rendering::OpenGL::PipelineBindings::color_attribute name. Inactive inputs may be omitted; an active color input is rejected for the uncolored layout. CE::Rendering::Geometry::Vertex2D keeps its existing storage; CE::Rendering::Geometry::Vertex2DColor is a separate immutable upload format. Pipeline and geometry layouts must match. Shaders interpret color values consistently with their selected straight/premultiplied blend mode; upload does not convert them. Geometry exposes immutable CPU-readable layout/topology/count metadata. Pipeline validation checks complete primitives and bounded ranges before publication; native drawing additionally checks vertex-array/program domain identity and the live current context. Program/image native domains are checked as well.

Common validation is CPU-only and reads stable inputs. Pipeline construction checks source paths for emptiness, supported layout/topology/state and parameter contracts; it does not read files, link or reflect a program. Material construction validates a partial custom-default layer, so required values and sampler collisions are checked when resolving a complete draw. CE::Rendering::Parameters::validate_values() permits missing keys and colliding units in a partial layer; CE::Rendering::Parameters::validate_resolved() requires required keys and distinct final sampler units, but does not prove that engine values actually came from a camera. Neither checks image domains, backend unit limits, numeric finiteness or alpha/scale ranges. Backend binding supplies its additional validation. A validation/resolution failure leaves caller inputs and immutable recipes unchanged.

Rectangular clipping

DrawStyle2D::clip resolves into a copied optional DrawPacket2D::clip. A CE::Rendering::ClipRegion2D retains top-left logical edges and positive logical viewport dimensions. Edges must be finite and ordered; equal edges mean empty. CE::Rendering::intersect_clip_rects combines nested rectangles in the same logical coordinate space. Clipping does not transform with the draw’s model matrix or change authored order.

Playback maps the retained extent to its current framebuffer. CE::Rendering::resolve_clip_region clamps to the logical viewport and rounds nonempty edges outwards (floor left/top, ceil right/bottom). Rectangles outside the viewport, zero-area rectangles and a zero-sized framebuffer produce no pixels. This is integer rectangular clipping; it does not promise subpixel masks or rotated clips. Resize/content-scale changes use the playback framebuffer and the retained logical extent without reading live UI state. A changed logical layout belongs to a newly published frame.

OpenGL converts top-left pixel edges to its bottom-left scissor box after validating the complete draw. Empty clips issue no draw; absent clips disable scissor. Full frame clears also disable scissor, so a preceding clipped packet cannot restrict the next clear. The renderer viewport supplies the framebuffer extent. Direct clipped CE::Rendering::OpenGL::Pipeline::draw() calls also supply that extent explicitly.

Material replacement

CE::Assets::Shaders::ProgramCache publishes executable program handles for explicit program access; CE::Rendering::DrawStyle2D stores an immutable material handle. CE::Assets::Materials::Manager::load() retains an existing key; reload() builds a full candidate outside cache locks, then replaces one material after success. Builders own their typed definitions/backend mappings and validate all dependent resources. A throw or null candidate retains the previous complete material; retained readers keep the old material/pipeline/image generation. Provider teardown clears materials before other resource caches, with the same loading-owner/domain exclusion. The demo loads indexed main:text and main:images recipes and reloads them on the platform owner. Frame preparation resolves asset/text submissions into owned packets; playback only consumes geometry, material, ranges, and copied parameters. A failed demo reload keeps the previous generation and reports its error in the overlay.

Graphics manifests 2.0 also produce owned CPU recipes through explicit index selection. Shader/material documents contain no default images; sprites and tilesets can select a material independently of their texture. The manifest guide owns the document format; the loader guide owns catalogue ordering, preserve-existing loads and incremental replacement.

Use architecture validation for native state/reload, retained-packet and fixture checks.

API domains

CE::Rendering::Pipelines::Definition describes owned program source paths, position3/UV2 or position3/UV2/color4 vertex layout, triangle/strip topology, fixed blend/depth/cull intent, and a public parameter contract. CE::Rendering::Materials::Definition references a specific shared pipeline generation and supplies defaults, including retained image handles and zero-based texture-unit requests. Neither definition contains GLSL uniform names. Backend mappings are supplied to explicit OpenGL builders.

CE::Rendering::Resources::Pipeline is a backend base with a validated definition snapshot and no value copying/slicing. Its backend subclass must retain the linked executable program; compiled stages remain transient. CE::Rendering::Resources::Material copies its CE::Rendering::Materials::Definition on construction. Definition/default access is read-only. Building another pipeline/material instance preserves the old logical generation while its readers retain it. CE::Assets::Materials::Manager publishes complete immutable materials, including their pipeline generation, through explicit application-supplied builders. Indexed recipes use CE::Assets::Shaders::Manager and the optional provider-owned CE::Assets::Shaders::Builder. Both paths preserve immutable generations. CE::Rendering::Frame packets retain those generations for low-level renderer playback.

Portable resource interfaces live in CE::Rendering::Resources. Geometry owns coordinates, vertex layouts and uploaded buffers; Pipelines owns fixed-state definitions and pass constraints; Materials owns retained material construction inputs. Parameters, Sampling and Submission describe copied draw values, immutable sampling and CPU packet resolution. Concrete native implementations belong to CE::Rendering::OpenGL.

The shader asset family preserves a separate distinction between recipes and their retained results:

API

Role

CE::Assets::Shaders::ProgramRecipe, MaterialRecipe

Owned CPU sources, identities, literals, contracts and backend payloads.

CE::Assets::Shaders::Program, Material

Immutable catalogue entries pairing a recipe with its executable or material generation; a material entry also retains its selected program entry.

CE::Rendering::Resources::Program, Material

Executable-program access and retained material resources used for drawing.

CE::Rendering::OpenGL::Program, Pipeline

Concrete linked OpenGL resources and validated native playback.

Legacy submission

Draw2D/iDraw/DrawInfo and the font formatting pointer contract are retired. Deprecated BitmapFont keeps immutable width metrics and typed normal/alternate-bank layout; callers place and rotate text through CE::Rendering::DrawStyle2D::model_matrix. Legacy AsciiFont uses a supplied font file and an ASCII atlas.

See BitmapFont deprecation.

Native bootstrap and binding

The OpenGL provider implements CE::Assets::Shaders::Builder without exposing GL types to common loading. It validates opaque bindings.opengl payloads and links the owned prepared source bytes. It constructs materials from the catalogue’s retained program recipe/executable pair. These optional APIs coexist with the manual builders below.

CE::Rendering::OpenGL::ResourceProvider::build_pipeline(definition, bindings) links the definition’s sources and constructs a native pipeline after validating reflection. PipelineBindings maps public parameter keys to uniform names; its attribute-name mapping is also backend-owned. build_material(recipe) checks that its pipeline and default images belong to this provider’s live native domain. Both builders require the platform owner with its rendering context current. Common resource/definition headers remain free of GL names and types.

The native pipeline owns the linked OpenGL program and immutable definition. Native stages are marked for deletion after attachment and detached after linking, including a failed link, so a retained executable does not retain its compilation stages. Failure before linking is covered by the program/stage cleanup guards.

Every contract key needs an explicit unique uniform mapping. Required uniforms must be active; optional ones may be absent after optimization. Active uniform types must match exactly. The initial API rejects uniform arrays and block storage, and requires every active uniform to appear in the contract. This prevents an undeclared mutable program value from silently affecting another draw.

CE::Rendering::Resources::Material::resolve() produces copied values from engine semantics and custom layers. Pass those values to CE::Rendering::OpenGL::Pipeline::bind_parameters() on the platform owner. The binder fills omitted active optional uniforms from contract defaults or the mapping’s missing_value, then validates the complete set, including fallback sampler collisions. It checks all image domains/units before changing the program or texture bindings. This method applies parameter/resource values only. The native pipeline’s draw() validates the complete geometry/state/parameter request, reapplies blend equations and factors, depth enable/function/write mask, cull enable/face, and CCW front-face winding, then binds parameters and geometry and draws. Every draw sets all supported state, so adjacent draws/passes cannot inherit another pipeline’s policy. Depth clears explicitly enable depth writes before clearing; the next draw reapplies its own mask.

PassConstraints2D restricts any selected pipeline state without overriding it. Default 2D passes require disabled depth; set or reset that constraint explicitly for a depth pass or a mixed-policy pass. Opaque blending uses one/zero factors; straight alpha uses source-alpha/one-minus-source-alpha RGB with one/one-minus-source-alpha alpha; premultiplied alpha uses one/one-minus-source-alpha; additive uses source-alpha/one RGB and one/one alpha. All use additive blend equations.

Recording-GLAD scenarios cover a native two-image effect, optional uniform reset after another draw, inactive optional uniforms without sprite roles, reflection/type/storage failures, invalid attributes/unlinked programs, and sampler domain/unit failures before any bind. These sources use synthetic IDs and restore all replaced entry points; they do not replace real-context acceptance.

Default asset materials

Sprite and tileset loading, whole-image graphic creation, Unicode text upload and legacy AsciiFont loading retain compatible materials from the selected resource provider. Ordinary drawing requires no application shader files, uniform mappings or material factory. The provider constructs defaults on its upload owner; CPU frame preparation only reads the retained handles.

Profile

Geometry and transforms

Builtin2D::Textured

Position3UV2 triangles for whole images and glyphs.

Builtin2D::TexturedStrip

Position3UV2 triangle strips for sprite and tileset cells.

Both sample the required public image parameter and apply projection, view, model, scale and alpha semantics. Upload paths select straight alpha, disabled depth/write and no culling. Explicit premultiplied material requests also scale RGB during alpha fading. These profiles preserve the image’s immutable sampling: color images keep their linear/mipmap defaults, while font atlases keep linear filtering without mipmaps. Choose a separate sampler for pixel art.

An explicit DrawStyle2D::material overrides the asset’s retained selection. Sprite/tileset manifest shader references select a loaded material instead of a default. Graphic creation, Unicode upload and AsciiFont loading also accept an explicit material handle; that bypasses default construction. These are ordinary composition choices and require no asset subclass. Custom materials still need compatible geometry, parameter and blend contracts. Low-level direct construction can omit a material, in which case submission needs a per-draw selection.

OpenGL embeds the shader sources and bindings. Triangle and strip variants share their live program when their alpha convention matches, while keeping distinct pipelines. Provider caches are weak; retained assets and published packets own the live generations. A provider without built-in support rejects a required default during upload. Explicit selected handles continue to use their existing construction path.

Give one object a custom material

Two objects can share one sprite and one material while supplying different shader parameters. This example draws two copies of First Game’s actor with different color tints. Keep the sprite definition’s shader unset so other draws can continue using its default material.

Under the application’s graphics/shaders/ directory, create tinted.vert:

#version 330 core
layout(location = 0) in vec3 in_Position;
layout(location = 1) in vec2 in_Texcoord;
uniform mat4 projectionMatrix;
uniform mat4 viewMatrix;
uniform mat4 modelMatrix;
uniform float in_Scale;
out vec2 texcoord;
void main() {
    gl_Position = projectionMatrix * viewMatrix * modelMatrix * vec4(in_Position * in_Scale, 1.0);
    texcoord = in_Texcoord;
}

Create tinted.frag beside it:

#version 330 core
in vec2 texcoord;
uniform sampler2D imageTexture;
uniform vec4 tint;
uniform float in_Alpha;
out vec4 color;
void main() {
    vec4 pixel = texture(imageTexture, texcoord);
    color = vec4(pixel.rgb * tint.rgb, pixel.a * tint.a * in_Alpha);
}

Create graphics/shaders/tinted.json. Sprite cells use position3_uv2 triangle strips; this differs from the colored triangles used by UI adapters. The public parameter key tint maps to this program’s tint uniform:

{
  "asset_class": "shader",
  "version": "2.0",
  "namespace": "game",
  "programs": [{
    "name": "tinted-program",
    "stages": {
      "vertex": "shaders/tinted.vert",
      "fragment": "shaders/tinted.frag"
    }
  }],
  "materials": [{
    "name": "tinted",
    "program": "game:tinted-program",
    "vertex_layout": "position3_uv2",
    "topology": "triangle_strip",
    "state": {"blend": "straight_alpha", "depth": "disabled", "depth_write": false, "cull": "none"},
    "parameters": [
      {"key": "projection", "type": "mat4", "semantic": "projection"},
      {"key": "view", "type": "mat4", "semantic": "view"},
      {"key": "model", "type": "mat4", "semantic": "model"},
      {"key": "scale", "type": "float", "semantic": "scale"},
      {"key": "alpha", "type": "float", "semantic": "alpha"},
      {"key": "image", "type": "sampler2d"},
      {"key": "tint", "type": "vec4", "default": [1.0, 1.0, 1.0, 1.0]}
    ],
    "bindings": {"opengl": {
      "attributes": {"position": "in_Position", "texcoord": "in_Texcoord"},
      "parameters": {
        "projection": "projectionMatrix", "view": "viewMatrix", "model": "modelMatrix",
        "scale": "in_Scale", "alpha": "in_Alpha", "image": "imageTexture", "tint": "tint"
      }
    }}
  }]
}

Add that document to the existing graphics/graphics-manifests.json index:

{
  "version": "2.0",
  "manifests": ["definitions/sprites.json", "shaders/tinted.json"]
}

Keep the normal sprite-loading path from First Game. Include <cheryl/core/resources/asset-management/shader-asset-mgr.h>, the draw/submission headers, <glm/gtc/matrix_transform.hpp>, <memory> and <stdexcept>. Retain a game member:

std::shared_ptr<const CE::Rendering::Resources::Material> tinted_;

On platform during init_platform(), after loader.load_assets(engine_.resources()), retrieve the uploaded material:

tinted_ = CE::Assets::Shaders::Manager::get().find_material("game:tinted");
if (!tinted_)
    throw std::runtime_error("The tinted material is required");

In frame preparation, replace First Game’s actor draw with the following two draws, keeping its actor_, pass and context. Each style supplies its own transform and copied parameter values:

CE::Rendering::DrawStyle2D first;
first.material = tinted_;
first.scale = 4.0f;
first.model_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(160.0f, 100.0f, 0.0f));
first.parameters["tint"] = glm::vec4(1.0f, 0.25f, 0.25f, 1.0f);
pass.add(CE::Rendering::Submission::resolve_sprite(*actor_->sprite, actor_->cell, first, context));

auto second = first;
second.model_matrix = glm::translate(glm::mat4(1.0f), glm::vec3(240.0f, 100.0f, 0.0f));
second.parameters["tint"] = glm::vec4(0.25f, 0.5f, 1.0f, 1.0f);
pass.add(CE::Rendering::Submission::resolve_sprite(*actor_->sprite, actor_->cell, second, context));

The first object appears red-tinted and the second blue-tinted. The sprite, material and program remain immutable and shared; each packet owns its resolved tint. The standard context inserts the sprite’s image automatically. Omitting a draw’s tint uses the contract’s white default. Game logic can keep tint alongside an object’s position and change it during update, then copy both into its next draw style. No inheritance hook or material factory is required.

Reset tinted_ with the game’s sprite handles during platform-owned deinit_platform(). Earlier frames retain their selected material and parameters. A later catalogue replacement does not change those packets or automatically replace this retained game handle; adopt a new handle explicitly when changing material generations.

Default UI materials

TGUI and RmlUi uploaders select compatible materials when called without a Materials argument:

pending = uploader.submit(platform, session.record());

Construct the uploader on the provider owner during initialization. The first nonempty upload requests defaults on that owner, and subsequent uploads retain and reuse them. Empty recordings need no material capability. TGUI selects StraightAlpha; RmlUi selects PremultipliedAlpha. Applications need no material factory or additional shader files for these defaults with OpenGL.

The generic contract is CE::Assets::Shaders::Builder::build_builtin_material(kind, blend), accessed through provider.shader_asset_builder(). The library helper CE::Assets::Shaders::build_builtin_material(provider, kind, blend) validates that the provider supplies a nonnull result. Engine defines CE::Rendering::Materials::Builtin2D; the graphics backend owns its realization:

Profile

Contract

SolidColor

Output vertex RGBA from Position3UV2Color4 triangles.

TexturedColor

Multiply sampled RGBA by vertex RGBA, with a required custom Sampler2D named image. Preserve the caller’s alpha convention.

These UI profiles use the required Mat4 projection semantic named projection, disabled depth/write/culling and the requested blend state. OpenGL embeds its shader sources and uniform mappings and constructs them through its existing validated recipe path. Its provider weakly caches programs by profile and materials by profile/blend. The UI alpha variants share the live program without changing each other’s state; uploader handles and published frames keep resources alive. The cache adds no global resource owner or catalogue entries.

Construction requires the provider’s live domain, platform owner and current graphics context, including a warm cache lookup. A provider without a shader builder, or a builder without built-in support, rejects the default request. UI modules depend on the generic Engine contract; they do not link OpenGL.

Custom UI materials

To customize rendering, author and load shader/material JSON recipes, then pass their material handles to the uploader. No application material factory is needed. For example, after loading game:ui-solid and game:ui-textured, select them during initialization:

CE::UI::TGUI::Materials custom;
auto& catalogue = CE::Assets::Shaders::Manager::get();
custom.solid = catalogue.find_material("game:ui-solid");
custom.textured = catalogue.find_material("game:ui-textured");
if (!custom.solid || !custom.textured)
    throw std::runtime_error("Custom UI materials are required");
custom.image_parameter = "image";

Include <cheryl/core/resources/asset-management/shader-asset-mgr.h>, <cheryl/ui/tgui/scene.h> and <stdexcept>. Retain custom with the view and pass it when publishing a recording:

pending = uploader.submit(platform, session.record(), custom);

RmlUi uses its own CE::UI::RmlUi::Materials with the same handle-selection pattern. Overrides must match the adapter’s colored triangles, projection semantic, image parameter and alpha/depth/culling contract. Choose the toolkit’s blend state in the JSON; RmlUi’s sampled/vertex RGBA must stay premultiplied. image_parameter and image_unit select the custom sampler key and binding unit. Other custom values come from material defaults or Materials::pass_parameters. The third argument bypasses default construction; explicitly passing empty or incompatible handles fails for draws that use them. It does not trigger fallback. Published packets retain the chosen generations through later catalogue changes.

Manual material construction

Direct typed construction remains available independently of UI defaults and indexed loading. Use it when code must construct a recipe explicitly; the normal custom UI path above can use JSON. On the OpenGL loading owner, with its context current, a textured TGUI material can be built as follows (native is a CE::Rendering::OpenGL::ResourceProvider&, root is the asset root):

CE::Rendering::Pipelines::Definition definition;
definition.vertex_layout = CE::Rendering::Geometry::Layout2D::Position3UV2Color4;
definition.topology = CE::Rendering::Geometry::Topology::Triangles;
definition.state = {CE::Rendering::Pipelines::Blend::StraightAlpha,
                    CE::Rendering::Pipelines::Depth::Disabled, false,
                    CE::Rendering::Pipelines::Cull::None};
definition.program_sources = {root / "graphics/shaders/tgui.vert",
                              root / "graphics/shaders/tgui-textured.frag"};
definition.parameters = {{"projection", CE::Rendering::Parameters::Type::Mat4, true,
                          CE::Rendering::Parameters::Semantic::Projection},
                         {"image", CE::Rendering::Parameters::Type::Sampler2D}};
CE::Rendering::OpenGL::PipelineBindings bindings{{{"projection", "projectionMatrix"},
                                                {"image", "mytexture"}}};
auto material = native.build_material({native.build_pipeline(std::move(definition), bindings), {}});

Supply that handle as Materials::textured to the UI uploader; its per-draw image binding supplies the texture and sampling without putting an image in the recipe. The solid material omits the image parameter and uses tgui-solid.frag. RmlUi uses the same construction pattern with premultiplied-alpha blending. Required parameter keys belong in the typed definition, and their uniform mappings belong in bindings; adding one requires both and a matching shader uniform.

Keep this manual API, example and its consumer coverage usable independently of automatic UI defaults. Other shader files are not indexed merely because they exist; their purposes and native behavior remain unverified.