Metadata-Version: 2.4
Name: vcti-shader-atom-lut
Version: 1.0.0
Summary: The atom-lut shader feature: the vertex-stage lookup that reads per-atom state from a client-owned table.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-atom-lut
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-atom-lut/blob/main/CHANGELOG.md
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: <3.15,>=3.12
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: vcti-shader-base>=1.0.0
Provides-Extra: gl
Requires-Dist: vcti-shader-compiler[gl]>=4.0.0; extra == "gl"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: vcti-shader-compiler>=4.0.0; extra == "test"
Requires-Dist: numpy; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# 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

```bash
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

```toml
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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

```python
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](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:

```python
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:

```python
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](docs/design.md) |
| Navigate or modify the source, including the Slang | [docs/source-guide.md](docs/source-guide.md) |

The full API reference is generated from the source docstrings and published in
the unified VCollab docs.
