-
Notifications
You must be signed in to change notification settings - Fork 11
RenderTable
RenderTable is Fio's dense render projection of the authored brush world.
It is one half of the renderer's T3 data boundary: the authored world remains object- and dictionary-oriented, while the renderer consumes a compact numerical representation built from that world.
A RenderTable contains one row per brush and stores render-relevant state in dense NumPy columns addressed by an integer slot.
The table is a derived execution representation, not a second world.
The authoring model is object-oriented. The execution model is data-oriented.
Before the dense render projection, Fio could cull brushes numerically and then immediately fall back to the authored brush dictionaries to determine what each visible brush actually was.
That meant the renderer repeatedly performed work such as:
- Python dictionary lookups
- shader/material classification
- string comparisons
- texture-name resolution
- UV-property resolution
- geometry classification
- dynamic-object checks
Much of that information changes only when a brush is edited.
RenderTable moves those stable decisions to a cache boundary. Frame-time rendering can then operate on integer slots, bit fields and numerical columns.
The important distinction is:
- Authoring state: the brush dictionaries owned by the world.
- Execution state: the numerical projection in RenderTable.
- GPU state: OpenGL resources maintained by the renderer.
The table does not replace the authored brush.
RenderTable stores resolutions of authored data.
Deleting the table and rebuilding it from the authoritative brush list must reproduce the same render representation.
The table is not edited as a second representation of the world.
A brush keeps its existing UUID as its identity.
A slot is only a dense numerical address valid for the table's current generation.
Frame code therefore operates on dense slot arrays rather than repeatedly translating a UUID through a Python dictionary.
The UUID remains the world identity; the slot is an execution address.
The table does not require an OpenGL context.
Texture names are interned to dense integer IDs. The renderer maps those IDs to actual OpenGL texture handles on the rendering thread.
This keeps the projection usable by headless tests and logic-side code without coupling it to GL lifetime.
The exact column set evolves with the renderer, but the principal groups are:
| Column | Purpose |
|---|---|
| center | Brush centre / translation |
| half | AABB half-extents |
| rot | Rotation axis and angle |
| class_bits | Numerical render/material classification |
| tex_name_id | Dense texture-name IDs for the six cube faces |
| uv_scale | Per-face UV scale |
| uv_angle | Per-face UV rotation |
| uv_shift | Per-face UV offset |
| uv_natural | Natural-scale flags |
| uv_has_scale | Explicit UV-scale flags |
| colour | Resolved brush colour |
| glow_colour | Resolved glow colour |
| geo_epoch | Geometry-cache version |
| geometry_id | Dense geometry handle |
| special-material columns | Water, glass and fog parameters |
The representation is intentionally numerical so consumers can gather rows with NumPy rather than materialising brush objects.
class_bits is a uint16 classification word.
Current brush classifications include:
- authored-hidden
- water
- fog
- glass
- glow
- trigger
- subtract
- custom/convex geometry
- textured
- shadow-caster
- dynamic mover/door
- texture tiling
These bits replace repeated semantic discovery in hot renderer paths.
For example, whether a brush is water is resolved when its cold render state is rebuilt rather than rediscovered from strings for every visible brush on every frame.
The table deliberately separates state by how frequently it changes.
Cold state is expensive to derive and changes only at editor/cache boundaries.
Examples:
- class_bits
- texture-name IDs
- UV state
- material parameters
- geometry classification
- geometry epoch
Cold state is rebuilt when the relevant world epoch or dirty-object boundary changes.
Warm state is cheap to refresh because transforms can change continuously.
Examples:
- center
- half
- rot
Movers and doors are the main reason these columns are refreshed during normal execution.
Some state cannot safely be cached.
The most important example is the live hidden flag.
BigWorld can park an object by changing its hidden state directly. A cached copy could therefore become stale without a structural world change.
The renderer reads the live hidden state at frame time.
This is deliberate: not everything should be forced into a cold cache.
Convex/custom brush geometry is represented by dense geometry handles and records rather than forcing the renderer to rediscover geometry from the authored brush during every draw.
Ordinary box brushes can use the shared cube geometry.
Brushes with their own convex geometry receive a geometry record containing the information required to prepare and draw that mesh.
The geometry epoch provides a numerical way for consumers to determine whether cached geometry is stale.
Texture names are interned into dense integer IDs.
The table pre-interns the common sentinel names:
- default.png
- caulk.jpg
- nodraw.jpg
A texture ID is therefore suitable for numerical classification and gathering.
The renderer performs the separate mapping:
texture-name ID → OpenGL texture ID
That separation is important because the logic-side table should not own GPU resources.
The normal conceptual flow is:
Authoritative brush dictionaries
|
v
RenderTable
|
+--> numerical visibility/classification
|
+--> packed render keys
|
+--> sorted equal-key runs
|
v
renderer passes
|
v
OpenGL draws
The renderer should not have to ask a brush dictionary what it is after the projection has already resolved that information.
RenderTable provides dense render state.
render_keys.py then turns relevant state into packed sortable integer keys.
The renderer can sort those keys numerically and identify contiguous runs with equal state.
Conceptually:
dense state
-> packed key
-> numeric sort
-> contiguous runs
-> GPU submissions
This is the bridge between classification and batching.
The goal is not to eliminate all Python loops. Small loops over already-formed draw runs are acceptable.
The goal is to eliminate Python work proportional to the number of world objects when the same work can be expressed as bulk numerical transformation.
The hot path should operate on:
- NumPy arrays
- integer slots
- classification masks
- packed keys
- contiguous runs
- reusable staging buffers
It should not repeatedly perform:
- object type discovery
- dictionary traversal
- string classification
- object-to-array conversion
- allocation of temporary arrays for every draw
- Python iteration over every visible brush
This is the practical form of Fio's dense numerical core.
RenderTable belongs to the world, not to an individual camera.
Multiple views therefore consume the same dense brush projection.
Each view owns its own:
- camera
- projection matrix
- view matrix
- viewport
- visibility/culling result
- depth ordering where required
- view-specific render state
The world projection is shared; view-dependent decisions are not.
This is essential for split-screen rendering because creating one object representation per view would recreate the very duplication the dense projection is intended to remove.
A table has a generation.
A structural reconciliation can change the slot mapping and increments the generation.
Consumers that retain slots must therefore treat slots as generation-scoped addresses.
The authoritative UUID remains valid across these cache-boundary changes.
A normal frame should not rebuild cold state unnecessarily.
The intended lifecycle is:
- Authoring/world state changes.
- The relevant epoch or dirty boundary changes.
- RenderTable reconciles.
- Cold columns are resolved.
- Dynamic transforms are refreshed.
- The renderer consumes dense rows.
A useful invariant for tests is:
Rebuilding RenderTable from the same authoritative brush state must produce the same render classification and material representation.
Tests should cover:
- empty worlds
- one-brush worlds
- mixed material classes
- water/fog/glass/glow/trigger brushes
- subtract brushes
- movers and doors
- custom convex geometry
- texture changes
- UV changes
- hidden/visible transitions
- geometry invalidation
- row insertion/removal
- slot remapping
- generation changes
- multiple render views
- split-screen
- headless construction without an OpenGL context
RenderTable is not:
- a replacement for the authored world
- a second editable scene graph
- a GPU resource manager
- an OpenGL object database
- a per-camera copy of the world
- a requirement to convert every Fio object into a Python wrapper
- a general-purpose ECS
It is a narrow execution projection designed around the renderer's actual data requirements.
RenderTable is the point where Fio stops asking the renderer to repeatedly interpret authored brush objects.
The world remains authoritative and human-editable.
The renderer receives a dense numerical projection.
From there, Fio can perform:
cull → classify → key → sort → run → draw
without repeatedly rediscovering the meaning of every brush.
That is the purpose of T3: not to replace Fio's object-oriented authoring model, but to prevent that model from becoming the CPU's frame-time execution model.