Metadata-Version: 2.4
Name: vcti-shader-base
Version: 2.3.0
Summary: The vocabulary a shader feature declares itself with: attribute, uniform, table, texture, sampler and output specs, and the ShaderDefinition record. Zero dependencies.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-base
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-base/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
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff; extra == "lint"
Provides-Extra: typecheck
Requires-Dist: mypy; extra == "typecheck"
Dynamic: license-file

# vcti-shader-base

The vocabulary a shader feature declares itself with: attribute, uniform, table, texture, sampler and output specs, and the ShaderDefinition record. Zero dependencies.

## Overview

Shaders in this system are compiled ahead of time.

A build step turns Slang sources into GLSL ES text. Whatever draws with the
result later — a web viewer, a test harness — did not compile it and cannot
inspect it. So it has to be *told* what the shader expects:

- which buffer belongs in each vertex attribute,
- which uniforms exist and how large they are,
- which integer selects which mode,
- which lookup table goes in which sampler, and in what format it is uploaded,
- and what the attachment it writes into has to be.

Writing that down is what this package is for.

A **shader feature** is one piece of composable shading math. Here are some
examples:

- `deform` moves geometry,
- `fringe` colors it by bands,
- `derive` computes a quantity from a source field — scalar, vector, 6-DOF or
  tensor,
- `mask` culls the submeshes a client-bound table marks hidden.

Each ships as its own installable package. Each declares what its own math needs,
and says what it is.

`vcti-shader-base` is the vocabulary for writing exactly that declaration, and
nothing more:

- **Field specs** — `AttributeSpec` (per-vertex inputs), `UniformSpec`
  (draw-constant values), `TableSpec` (lookup tables fetched exactly),
  `TextureSpec` with `SamplerSpec` (images sampled between texels), `OutputSpec`
  (fragment outputs).
- **The definition** — `ShaderDefinition`, with `StageRole`: how a feature names
  the stage it runs in, the Slang modules it ships, and the capability tags it
  introduces.

Declaring a feature needs **nothing else** — no compiler, no build toolchain, no
other package.

## Installation

```bash
pip install vcti-shader-base
```

Requires Python 3.12, 3.13, or 3.14. No runtime dependencies.

### In `requirements.txt`

```
vcti-shader-base>=2.3.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-base>=2.3.0",
]
```

## Quick Start

### Declare what the shading math needs

Consider a structural analysis. A CAE solver reports how far each node of a mesh
moves under a load, and we want to draw the deformed shape.

Every vertex carries two values — where it sits, and how far it moved. Both vary
per vertex, so both are **attributes**. We also want to exaggerate the movement,
scaling it independently in x, y and z; that factor is the same for every vertex
in the draw, so it is a **uniform**:

```python
from vcti.shader.base import AttributeSpec, UniformSpec

inputs = (
    AttributeSpec("a_position", "vec3", "coordinates"),
    AttributeSpec("a_deformation", "vec3", "deformation"),
)
uniforms = (UniformSpec("u_deformScale", "vec3"),)
```

`a_position` and `a_deformation` are the names the *shader source* uses. The
third argument is the **semantic**, and it is what makes the declaration useful
to a caller. A caller has buffers of its own — node coordinates, a displacement
field — and must know which one goes where. The name cannot answer that: it is a
shader-source detail and can be renamed. The semantic names the data instead, so
`coordinates` means mesh node positions and `deformation` means the displacement
vector.

Two optional fields are worth knowing. `array_length` stays a **number** so Python
can size a buffer, and `gl_type` joins it onto the type only where the type is
emitted:

```python
UniformSpec("u_bandColors", "vec4", array_length=8).gl_type   # 'vec4[8]'
UniformSpec("u_deformScale", "vec3").gl_type                  # 'vec3'
```

`named_values` turns an integer a caller would otherwise hard-code into something
nameable:

```python
mode = UniformSpec("u_deformMode", "int", named_values={"displacement": 0, "rotation": 1})
mode.named_values["rotation"]      # 1 — the value to write
```

Data with one row per addressable thing — per submesh, per component — is too
large and too model-dependent for uniforms, so a feature reads it from a texture
and declares that too:

```python
from vcti.shader.base import TableSpec

tables = (TableSpec("u_maskLut", "Texture2D<uint4>", format="r8ui", semantic="mask"),)
```

`type` is the declaration as the feature writes it in Slang, not as one target
renders it. `format` names the texture to upload — a sampler type carries
signedness and dimensions but never channel count or bit depth — and it is
spelled as a GLSL layout qualifier, which is also SPIR-V's image format, because
a sampled texture carries no format in the shader languages at all. It is
therefore nobody's API constant: `r8ui` is `R8UI` to GL and `r8uint` to WebGPU,
and a consumer maps it as it maps `vec3`. And `semantic`
matters more here than on an attribute: emitting GLSL ES pairs the texture with
a dummy sampler and names the combination itself, so the declared name is *not*
what reaches the program, and the semantic is what a caller matches on.

Every texture declared this way is an unfiltered lookup table — read with
`texelFetch`, filtered `NEAREST`, no mipmaps. Addressing is the feature's own:
a width to wrap at, a stride, a slot, each an ordinary uniform.

A table is fetched at exact coordinates. An authored image is *sampled* between
them, which takes a second declaration — the sampler it is read through, shared
by every slot that wants the same filtering:

```python
from vcti.shader.base import SamplerSpec, TextureSpec

samplers = (
    SamplerSpec(
        "trilinear",
        mag_filter="linear",
        min_filter="linear",
        mipmap_filter="linear",
        wrap_s="repeat",
        wrap_t="repeat",
    ),
)
slots = (
    TextureSpec(
        "u_baseColor",
        semantic="base-color",
        kind="2d",
        component="float4",
        format="rgba8unorm-srgb",
        requires_mip_chain=True,
        sampler="trilinear",
        uv="uv.0",
        selection="draw",
        neutral=(1.0, 1.0, 1.0, 1.0),
    ),
)
```

Whether the slot is an *array* is not declared: it follows from `selection`, so
the two cannot disagree. `neutral` is what a component that does not use the slot
reads — four linear floats bound as a 1×1, or `None` meaning the feature branches
instead. And `format` carries color space in the token, which is why there is no
separate field for it: `r32float-srgb` is not a token, so a consumer catches the
contradiction by vocabulary membership rather than by a compatibility matrix.
Nothing here checks it — `format` is a free-form string like every other.

A feature in the fragment stage also declares what it writes:

```python
from vcti.shader.base import OutputSpec

outputs = (OutputSpec("fragColor", "vec4"),)
targets = (OutputSpec("pickTarget", "uvec4", format="rgba32ui"),)
```

`format` says what the attachment has to be, in the same spelling `TableSpec`
uses. It is optional, and omitting it is a claim rather than a gap: *no exact
format required*, which is what a color written to the default framebuffer means.
Not *any attachment will do* — `type` already demands a matching numeric category,
and a `vec4` output against an integer attachment is rejected outright. What
`format` adds is the channel count and the bit depth, which is the half that fails
quietly: a `uvec4` output says nothing about bit depth, so an `rgba8ui` attachment
satisfies the type and truncates every value written.

### Describe the feature itself

The specs say what the shading math needs. A `ShaderDefinition` says what the
feature *is*. Each feature constructs exactly one and exports it as `DEFINITION`:

```python
from pathlib import Path
from vcti.shader.base import ShaderDefinition, StageRole

DEFINITION = ShaderDefinition(
    id="deform",
    role=StageRole.VERTEX,
    capabilities=("deform3", "deform6"),
    slang_modules=("deform.slang",),
    slang_dir=Path(__file__).parent / "slang",
    description="deform3 scaled displacement; deform6 Rodrigues rotation.",
)
```

- **`role`** is where the feature's math runs: `VERTEX` per vertex, before
  rasterization; `FRAGMENT` per fragment, after it. It says where and not what —
  a feature in the fragment stage may write the shader's output or may compose
  with the one that does.
- **`capabilities`** are the tags this feature offers. They are opaque strings and
  each feature owns its own, so adding one needs no release of this package.
- **`requires`** are the tags this feature needs *another* feature to offer —
  empty for almost every feature, and non-empty for one whose Slang imports
  another's module and calls into it. Tags rather than ids, so whatever offers
  the tag satisfies it. Nothing here checks that anything does: that is a
  question about a *set* of features, so the composing layer answers it, and
  decides whether an unmet requirement pulls the other feature in or rejects the
  request. Keyword-only, being a fourth tuple of strings next to `capabilities`.
- **`slang_modules`** names the Slang modules this feature publishes for a shader
  to import. **`slang_dir`** says where they live — and it is the field with teeth:
  the `.slang` files are installed inside the feature's own package, so only the
  feature can resolve the directory, and the build passes it to the compiler as an
  `import` search path. This package only records the path; it never opens it, so
  confirming the files are really there is the feature's own test's job.

A feature that stops here is complete: it constructs one `ShaderDefinition`,
exports it as `DEFINITION`, and declares the specs its math needs.

## Type Reference

| Type | Fields | Notes |
|---|---|---|
| `AttributeSpec` | `name`, `type`, `semantic` | A per-vertex input, and what its data *is* |
| `UniformSpec` | `name`, `type`, `array_length=None`, `named_values=None` | `gl_type` joins the array suffix; `named_values` only on dispatch uniforms |
| `TableSpec` | `name`, `type`, `format`, `semantic` | An unfiltered lookup table; `type` is the Slang declaration, `format` what the client uploads |
| `TextureSpec` | `name`, `semantic`, `kind`, `component`, `format`, `requires_mip_chain`, `sampler`, `uv`, `selection`, `neutral` | One sampled image slot; arrayness follows from `selection` |
| `SamplerSpec` | `name`, `mag_filter`, `min_filter`, `mipmap_filter`, `wrap_s`, `wrap_t` | How a texture is read, shared by the slots that name it |
| `OutputSpec` | `name`, `type`, `format=None` | A fragment output; `format` constrains the attachment, `None` means it does not |
| `ShaderDefinition` | `id`, `role`, `capabilities`, `slang_modules`, `slang_dir`, `requires=()`, `description=""` | One feature's self-declaration |
| `StageRole` | `VERTEX`, `FRAGMENT` | Where the feature's math runs |

Every type is immutable and compared by value, and every one is hashable — so
specs can go in a set or a dict key, and duplicates drop out on their own.

## Dependencies

None — the standard library covers it. A feature can declare its specs and its
definition without installing a compiler or a build toolchain behind it.

Development extras: `test` (pytest, pytest-cov), `lint` (ruff), `typecheck`
(mypy).

## Documentation

| If you want to… | Read |
|---|---|
| Get started using the package | Quick Start above |
| Build and ship a complete feature, and avoid the pitfalls | [docs/patterns.md](docs/patterns.md) |
| Understand what these types describe and why | [docs/design.md](docs/design.md) |
| Navigate or modify the source | [docs/source-guide.md](docs/source-guide.md) |
