RenderTarget over tc_texture migration plan¶
Date: 2026-04-22
Goal¶
Move tc_render_target output onto tc_texture rails without changing FBOPool.
The target model:
tc_render_targetowns persistent output textures:color_texture: tc_texture_handledepth_texture: tc_texture_handle- Materials keep using ordinary
tc_texture_handleslots. - Dragging a render target into a material texture slot assigns the render target's color
tc_texture. - Rendering writes final pipeline output into GPU storage owned by the render target's
tc_texture. FBOPoolremains the private intermediate storage for pipeline framegraph resources.
Current State¶
tc_texture is a runtime resource, not an asset. TextureAsset is the asset wrapper around TcTexture.
Current tc_texture still assumes CPU pixel data:
tc_texture.datawidth,height,channels,formattc_texture_upload_gpu()requirestex->data
Current render target output is not tc_texture based:
RenderingManagerstores render target GPU state inrender_target_states_.ViewportRenderStateownsoutput_color_texandoutput_depth_texastgfx::TextureHandle.RenderEngine::ViewportContextreceives final output as rawtgfx::TextureHandle.
Current material texture binding is already close to the desired API:
tc_material_texturestorestc_texture_handle.- material binding calls
wrap_tc_texture_as_tgfx2(device, mat_tex.texture).
The main missing piece is support for GPU-only/renderable tc_texture.
Non-Goals¶
- Do not rewrite
FBOPoolin this migration. - Do not make pipeline intermediate resources
tc_textureyet. - Do not introduce a separate material
TextureSourceABI unlesstc_textureproves insufficient. - Do not make render targets file assets.
Phase 1: Extend tc_texture Metadata¶
Add storage and usage metadata to tc_texture.
Suggested enums:
typedef enum tc_texture_storage_kind {
TC_TEXTURE_STORAGE_CPU_PIXELS = 0,
TC_TEXTURE_STORAGE_GPU_ONLY = 1,
} tc_texture_storage_kind;
typedef enum tc_texture_usage_flags {
TC_TEXTURE_USAGE_SAMPLED = 1 << 0,
TC_TEXTURE_USAGE_COLOR_ATTACHMENT = 1 << 1,
TC_TEXTURE_USAGE_DEPTH_ATTACHMENT = 1 << 2,
TC_TEXTURE_USAGE_COPY_SRC = 1 << 3,
TC_TEXTURE_USAGE_COPY_DST = 1 << 4,
} tc_texture_usage_flags;
Add fields:
uint8_t storage_kind;
uint32_t usage;
Default for existing textures:
storage_kind = TC_TEXTURE_STORAGE_CPU_PIXELS;
usage = TC_TEXTURE_USAGE_SAMPLED;
Add small C API helpers:
TC_API void tc_texture_set_storage_kind(tc_texture* tex, tc_texture_storage_kind kind);
TC_API void tc_texture_set_usage(tc_texture* tex, uint32_t usage);
TC_API void tc_texture_set_size_format(tc_texture* tex, uint32_t w, uint32_t h, tc_texture_format fmt);
TC_API bool tc_texture_is_gpu_only(const tc_texture* tex);
Expected files:
termin-graphics/include/tgfx/resources/tc_texture.htermin-graphics/src/resources/tc_texture_registry.c
Phase 2: Add GPU-Only tgfx2 Resolution¶
Teach the texture bridge to resolve both CPU-pixel textures and GPU-only renderable textures.
Current behavior:
wrap_tc_texture_as_tgfx2()uploads/wraps CPU pixel data.- Non-GL path errors when
tex->data == NULL.
Required behavior:
- CPU textures keep the existing path.
- GPU-only textures create or reuse a real
tgfx::TextureHandleusingtc_texturesize/format/usage. - Recreate when
tc_texture.header.versionchanges. - Cache by
(tc_texture pool_index, tgfx::IRenderDevice*).
Add or adapt:
tgfx::TextureHandle ensure_tc_texture_tgfx2(
tgfx::IRenderDevice& device,
tc_texture_handle handle
);
Then make wrap_tc_texture_as_tgfx2() use that path for GPU-only textures.
Expected files:
termin-render/include/termin/render/tgfx2_bridge.hpptermin-render/src/tgfx2_bridge.cpp- possibly
termin-graphics/src/tgfx_resource_gpu.cif the legacytc_gpuAPI also needs renderable storage support immediately.
Important ownership rule:
- CPU texture OpenGL wrappers remain temporary external handles, released by the caller.
- GPU-only
tc_texturehandles are owned by the texture bridge/cache and must not be destroyed by material binding.
Phase 3: Make tc_render_target Own Textures¶
Extend render target pool storage:
tc_texture_handle* color_textures;
tc_texture_handle* depth_textures;
On tc_render_target_new():
- create color texture
- create depth texture
- mark both
TC_TEXTURE_STORAGE_GPU_ONLY - assign usage and format
- set owner/name metadata
Suggested usage:
color.usage =
TC_TEXTURE_USAGE_SAMPLED |
TC_TEXTURE_USAGE_COLOR_ATTACHMENT |
TC_TEXTURE_USAGE_COPY_SRC |
TC_TEXTURE_USAGE_COPY_DST;
depth.usage =
TC_TEXTURE_USAGE_SAMPLED |
TC_TEXTURE_USAGE_DEPTH_ATTACHMENT;
Add API:
TC_API tc_texture_handle tc_render_target_get_color_texture(tc_render_target_handle h);
TC_API tc_texture_handle tc_render_target_get_depth_texture(tc_render_target_handle h);
TC_API void tc_render_target_ensure_textures(tc_render_target_handle h);
tc_render_target_set_width() and tc_render_target_set_height() must update texture metadata and bump texture versions.
Expected files:
termin-render/include/render/tc_render_target.htermin-render/src/tc_render_target.c
Phase 4: Render Into RenderTarget-Owned Textures¶
Replace render target output allocation in RenderingManager.
Current pattern:
ViewportRenderState* state = get_or_create_render_target_state(rt);
state->ensure_output_textures(*device, w, h);
ctx.output_color_tex = state->output_color_tex;
ctx.output_depth_tex = state->output_depth_tex;
Target pattern:
tc_render_target_ensure_textures(rt);
tc_texture_handle color = tc_render_target_get_color_texture(rt);
tc_texture_handle depth = tc_render_target_get_depth_texture(rt);
ctx.output_color_tex = ensure_tc_texture_tgfx2(*device, color);
ctx.output_depth_tex = ensure_tc_texture_tgfx2(*device, depth);
Primary functions to update:
RenderingManager::render_render_target_offscreenRenderingManager::render_viewport_offscreenRenderingManager::render_scene_pipeline_offscreen, where viewport output is backed by a render target
Expected files:
termin-engine/src/rendering_manager.cpptermin-engine/include/termin/render/rendering_manager.hpp
ViewportRenderState can remain for transition and non-render-target viewport output. It should stop being the owner for render target output.
Phase 5: Keep Material Binding Generic¶
Material binding should remain based on tc_texture_handle.
Desired invariant:
wrap_tc_texture_as_tgfx2(device, mat_tex.texture)
works for both:
- CPU pixel textures
- render target-owned GPU-only textures
No render-target special case should be needed in ColorPass or MaterialPass.
Expected files:
termin-app/cpp/termin/render/material_ubo_apply.cpptermin-render/src/tgfx2_bridge.cpp
Phase 6: Python Bindings¶
Expose render target output textures:
rt.color_texture
rt.depth_texture
or methods:
rt.get_color_texture()
rt.get_depth_texture()
The returned value should be usable with the existing material API:
phase.set_texture("u_reflection", rt.color_texture)
Expected file:
termin-render/python/tc_render_target_bindings.cpp
Phase 7: UI Drag-and-Drop¶
For tcgui:
ViewportListWidgetTcguialready has render target nodes.- Make render target nodes draggable or otherwise provide a drag payload.
- Drop payload can contain render target handle/name, then resolve to
rt.color_texture.
For Qt:
- Extend
EditorMimeTypeswith:
RENDER_TARGET_REF = "application/x-termin-render-target-ref"
- Add create/parse helpers.
Material inspector:
- Current
_TextureEditorlists only asset textures. - For MVP, add drop support without changing combo contents.
- On drop render target:
phase.set_texture(uniform_name, render_target.color_texture)
Later UI improvement:
Texture slot:
Assets
brick.png
Render Targets
Mirror.color
Minimap.color
Expected files:
termin-app/termin/editor_tcgui/viewport_list_widget.pytermin-app/termin/editor_tcgui/material_inspector.pytermin-app/termin/editor/drag_drop.py- Qt equivalents if the Qt editor path is still maintained
Phase 8: Material Serialization¶
Current .material serialization saves only TextureAsset UUIDs.
For render target textures, there is no TextureAsset. Add a separate section for non-asset texture references.
MVP format:
{
"textures": {
"u_albedo": "texture-asset-uuid"
},
"texture_refs": {
"u_reflection": {
"kind": "render_target",
"target": "MirrorRT",
"channel": "color"
}
}
}
Alternative future format:
{
"textures": {
"u_albedo": {
"kind": "asset",
"uuid": "texture-asset-uuid"
},
"u_reflection": {
"kind": "render_target",
"target": "MirrorRT",
"channel": "color"
}
}
}
Prefer the MVP format first to avoid breaking old material files.
Expected file:
termin-app/termin/assets/material_asset.py
Phase 9: Scheduling Dependencies¶
Even when render target output is represented as tc_texture_handle, the producer render target must be rendered before consumers.
Add dependency discovery:
- scan material phase textures
- identify textures owned by render targets
- render producer targets before current target/viewport
Possible owner metadata:
tc_render_target_handle owner_render_target;
stored in tc_texture, or a reverse lookup in the render target pool.
Suggested API:
TC_API bool tc_texture_get_owner_render_target(
tc_texture_handle texture,
tc_render_target_handle* out
);
MVP scheduling:
- Before rendering regular viewports, render all render targets referenced by scene materials.
- Later replace with a DAG/topological sort so render targets can depend on other render targets.
Expected files:
termin-engine/src/rendering_manager.cpp- material/drawable traversal helpers as needed
Test Plan¶
Start with a small render path test before UI work.
- Create a render target.
- Verify it creates valid color/depth
tc_texture_handles. - Render into the render target.
- Bind
rt.color_texturein a material texture slot. - Render a viewport using that material.
- Resize the render target.
- Verify the texture metadata and GPU handle update.
- Delete the render target and verify material references fail gracefully.
Regression checks:
- ordinary TextureAsset material textures still render
- material save/load preserves existing
"textures"format - render target-backed texture does not require CPU pixel data
- no stale tgfx2 handles after render target resize
Key Invariants¶
tc_texturedoes not require CPU data.tc_texturecan represent renderable sampled GPU storage.tc_render_targetowns and publishes output textures.- Materials remain consumers of
tc_texture_handle. RenderEnginereceives final output astgfx::TextureHandle, but ownership is rooted intc_texture.FBOPoolremains unchanged in this migration.