vcti-shader-atom-lut
The atom-lut shader feature: the vertex-stage lookup that reads per-atom state from a client-owned table.
Overview
A viewer needs to hide, show, isolate, select, recolour and drag parts of a mesh around while the user works. Rebuilding geometry for each of those is slow and gets slower as the model grows, so this feature does it differently: the mesh is divided once into atoms — the smallest addressable render units — and the client keeps a row of state per atom, indexed by a dense atom-id that every vertex carries as an attribute.
Each of those interactions is then one texture write. No geometry is rebuilt, no buffer repacked, and the cost does not depend on how the affected atoms are scattered through the mesh.
vcti-shader-atom-lut is the shader half of that arrangement. It ships the
Slang that addresses the table and reads its slots, a Python mirror of the same
layout so a caller can build and inspect a row, the specs saying what the lookup
needs bound, and the ShaderDefinition saying what the feature is.
Everything here is a declaration or fixed shader source. Nothing compiles or runs a shader; a build step does that, using what this package declares.
Installation
pip install vcti-shader-atom-lut
Requires Python 3.12, 3.13, or 3.14, matching vcti-shader-base. Nothing native
is built on that path, and nothing native is built by [test] either — only the
[gl] extra pulls a GL binding, and only on 3.14 does that compile from source
for want of a cp314 wheel.
In requirements.txt
vcti-shader-atom-lut>=1.0.0
In pyproject.toml dependencies
dependencies = [
"vcti-shader-atom-lut>=1.0.0",
]
Quick Start
Build an atom's row
The client owns the table, so building its rows is the first thing a caller does. A row is the flags, plus whichever payloads the deployment uses:
import math
from vcti.shader.atomlut import Material, Transform, axis_angle, pack_row
row = pack_row(
visible=True,
material=Material(diffuse=(200, 40, 40, 255), ambient=(20, 4, 4), shininess=64),
transform=Transform(
translation=(1.0, 2.0, 3.0),
rotation=axis_angle((0.0, 0.0, 1.0), math.pi / 2),
scale=2.0,
),
)
assert len(row) == 12
The transformed flag is derived, never passed — it is true exactly when a
transform is supplied that is not the identity, which is what keeps the flag and
the slots from disagreeing:
from vcti.shader.atomlut import atom_transformed, atom_visible
assert atom_visible(row[0])
assert atom_transformed(row[0])
assert not atom_transformed(pack_row(transform=Transform())[0])
Read it back
The same layout the shader reads, read in Python — which is what lets a caller answer questions without a GPU, and what the shader tests diff against:
from vcti.shader.atomlut import unpack_material, unpack_transform
assert unpack_material(row).diffuse == (200, 40, 40, 255)
placed = unpack_transform(row).apply((1.0, 0.0, 0.0))
assert [round(component, 6) for component in placed] == [1.0, 4.0, 3.0]
Visibility is a bit test — flags & 1 — and the shader's test is the same one,
so there is no threshold the two sides can disagree about.
Upload only what you use
A row is stride consecutive words. The stride a client uploads is decided by
the behaviours its pipeline enables, and the shader reads only the slots those
need:
from vcti.shader.atomlut import stride_for
assert stride_for(["visibility"]) == 1 # 4 bytes per atom
assert stride_for(["visibility", "atom-material"]) == 4 # 16 bytes per atom
assert stride_for(["visibility", "atom-transform"]) == 12 # 48 bytes per atom
The table is two-dimensional, because a single row would cap the atom count at
whatever MAX_TEXTURE_SIZE a device reports. A caller picks a width, and an
address wraps across texture rows:
from vcti.shader.atomlut import address, texel
word = address(1000, 0, 12) # atom 1000, slot 0, stride 12
assert word == 12_000
assert texel(word, 2048) == (1760, 5)
Upload the words as an R32UI texture of that width with NEAREST filtering,
and pass the width and stride as u_atomLutWidth and u_atomLutStride.
What a build step binds
from vcti.shader.atomlut import vertex_attributes, vertex_uniforms
attributes = vertex_attributes()
assert attributes[0].name == "a_atomId"
assert attributes[0].semantic == "atom-id"
assert [u.name for u in vertex_uniforms()] == ["u_atomLutWidth", "u_atomLutStride"]
The table sampler itself is not in there — a texture input cannot yet be
expressed in vcti-shader-base, so it is contract rather than spec. See
docs/design.md.
Select a pipeline
One tag per behaviour, plus one naming the table encoding. A consumer asks for the behaviours it needs and the format it uploads:
from vcti.shader.atomlut import DEFINITION, Encoding, capability_for_encoding
assert DEFINITION.role.value == "vertex"
assert capability_for_encoding(Encoding.R32UI) == "atom-lut-r32ui"
assert set(DEFINITION.capabilities) == {
"visibility",
"atom-material",
"atom-transform",
"atom-lut-r32ui",
}
Before emitting a shader, check the encoding can carry the slots those behaviours need. A 16-bit table holds the flags but no payload slot:
from vcti.shader.atomlut import check_encoding, slots_exceeding
check_encoding(Encoding.R32UI, ["visibility", "atom-material"]) # returns None
check_encoding(Encoding.R16UI, ["visibility"]) # flags fit
assert slots_exceeding(Encoding.R16UI)
The row
| Slot | Holds | Needed by |
|---|---|---|
| 0 | flags — visible, selected, dead, transformed | visibility |
| 1 | diffuse RGBA | atom-material |
| 2 | ambient RGB + shininess | atom-material |
| 3 | specular RGB | atom-material |
| 4-6 | translation | atom-transform |
| 7-10 | rotation quaternion, scalar last | atom-transform |
| 11 | uniform scale | atom-transform |
Colours are bytes; the transform is float32 bits, recovered in the shader by
reinterpretation, which is exact.
API surface
| Name | What it is |
|---|---|
DEFINITION |
the ShaderDefinition a build step imports to compose this feature |
SLANG_DIR, SLANG_MODULES |
the installed Slang directory, and the modules in it |
pack_row, unpack_material, unpack_transform |
build and read an atom's row |
Material, Transform, axis_angle |
the payloads, and a rotation helper |
pack_flags, atom_visible, atom_selected, atom_dead, atom_transformed |
the state word |
SLOTS, FULL_STRIDE, address, texel, rows_needed |
the layout and its addressing |
stride_for |
the smallest stride a set of behaviours needs |
Encoding, IMPLEMENTED_ENCODINGS, word_bits |
the table encodings and their capacity |
check_encoding, slots_exceeding |
reject an encoding too narrow for a slot |
capability_for_encoding, slang_module_for_encoding |
the tag and module naming one |
Dependencies
vcti-shader-base is the only runtime dependency — declaring a feature is pure
data. vcti-shader-compiler>=4.0.0 and numpy are test-only, and a separate
gl extra adds the GL binding the shader tests need to execute rather than
skip.
Documentation
| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| Understand the table contract and the decisions behind it | docs/design.md |
| Navigate or modify the source, including the Slang | docs/source-guide.md |
The full API reference is generated from the source docstrings and published in the unified VCollab docs.
Metadata
Release files for vcti-shader-atom-lut 1.0.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| vcti_shader_atom_lut-1.0.0.tar.gz | 52.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| vcti_shader_atom_lut-1.0.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 77.4 kB
Release files / vcti_shader_atom_lut-1.0.0.tar.gz
| Download URL | vcti_shader_atom_lut-1.0.0.tar.gz |
|---|---|
| Size | 52.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
fcec1013c230c8c081427983b2ba4861d446130216592def63af4561042115d7
|
|
BLAKE2b-256 checksum How to use checksums |
023b7a18d3d53bd25571ba93f1355963679d16841d1da9f9132347f2816563eb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency logRelease files / vcti_shader_atom_lut-1.0.0-py3-none-any.whl
| Download URL | vcti_shader_atom_lut-1.0.0-py3-none-any.whl |
|---|---|
| Size | 25.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
4e7d9ee77e8950113e2d2a76e6049f202e85a146f7742a7154fe856486c2067b
|
|
BLAKE2b-256 checksum How to use checksums |
5ecd56f78a5d448e167265eaf259143cb02d9af6484db1d3b41cea40f1487f07
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 13, 2026.
Transparency log