Metadata-Version: 2.4
Name: vcti-shader-mask
Version: 1.1.0
Summary: The mask shader feature: the vertex-stage cull that draws only the submeshes a client-owned mask marks.
Author: Visual Collaboration Technologies Inc.
License-Expression: LicenseRef-Proprietary
Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-mask
Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-mask/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>=2.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-mask

The mask shader feature: the vertex-stage cull that draws only the submeshes a client-owned mask marks.

## Overview

A viewer needs to hide and show components of a mesh while the user works — and
in a results viewer, to hide an element set the solver reported on, which is
finer than a component. 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 **submeshes** — any subsets the client wants to address as
units — every vertex carries the id of the one it belongs to, and the client
keeps a **mask** of one byte per submesh, zero for hidden and one for drawn. The
shader fetches each vertex's byte and collapses the vertex outside the clip
volume when it is zero.

Hiding any scattered set of submeshes is then one texture write. No geometry is
rebuilt, no buffer repacked, and the cost does not depend on how the affected
submeshes are scattered through the mesh.

Which id the mask is indexed by, and what the mask means, are the client's
choices per pass. A normal pass binds the visibility mask. A selection-outline
pass binds the selection mask to the same shader and draws only the selected
submeshes into an outline target. An isolate mode binds a third. The feature
culls; the renderer composes passes.

`vcti-shader-mask` is the shader half of that arrangement. It ships the Slang
that addresses the mask and applies its rule, a Python mirror of the same rule
so a caller can build a mask and read one back, the specs saying what the cull
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-mask
```

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-mask>=1.1.0
```

### In `pyproject.toml` dependencies

```toml
dependencies = [
    "vcti-shader-mask>=1.1.0",
]
```

## Quick Start

### Build a mask

The client owns the mask, so building it is the first thing a caller does. One
value per submesh, in id order:

```python
from vcti.shader.mask import DRAWN, HIDDEN, pack_mask

visible = pack_mask([True, True, False, True])
assert visible == (DRAWN, DRAWN, HIDDEN, DRAWN) == (1, 1, 0, 1)
```

The shader's rule is an integer compared with zero, and the Python mirror is the
same rule — any non-zero value draws, so a client that writes 255 is not wrong:

```python
from vcti.shader.mask import is_drawn

assert not is_drawn(0)
assert is_drawn(1) and is_drawn(255)
```

A value a byte cannot hold is refused rather than truncated, because 256 would
upload as 0 and hide a submesh the caller meant to draw:

```python
from vcti.shader.mask import check_value

check_value(255)
try:
    check_value(256)
except ValueError as error:
    assert "outside 0-255" in str(error)
```

### Upload it

The mask is an `R8UI` texture, one texel per submesh, with `NEAREST`
filtering. The id is the address; there is no stride. The texture is
two-dimensional, because a single row would cap the submesh count at whatever
`MAX_TEXTURE_SIZE` a device reports; wrapped across rows, the cap is that
number squared. A caller picks a width and the address wraps:

```python
from vcti.shader.mask import rows_needed, texel

assert texel(14_000, 2048) == (1712, 6)
assert rows_needed(50_000, 2048) == 25
```

Both refuse a width that is not positive, and a negative id or count. The
shader divides by the same width, so a bad value would otherwise surface on the
GPU as a wrong texel with nothing to say so:

```python
try:
    texel(0, 0)
except ValueError as error:
    assert "not positive" in str(error)
```

Pass the width as `u_maskLutWidth`. An unwritten or incomplete texture reads as
zero and hides everything, which is the loud failure this design chooses over a
quiet one.

### What a build step binds

Three things: the id attribute, the table, and the width.

```python
from vcti.shader.mask import vertex_attributes, vertex_tables, vertex_uniforms

(attribute,) = vertex_attributes()
assert (attribute.name, attribute.type, attribute.semantic) == ("a_maskId", "int", "mask-id")

(table,) = vertex_tables()
assert (table.name, table.type, table.format, table.semantic) == (
    "u_maskLut",
    "Texture2D<uint4>",
    "r8ui",
    "mask",
)

(uniform,) = vertex_uniforms()
assert uniform.name == "u_maskLutWidth"
```

The attribute is named for the feature, not for what the id counts. The client
binds whichever id buffer it likes to it — a submesh id, a mesh-component id —
and where
another feature is keyed by the same id, binds the same buffer to that feature's
attribute too.

The table's `type` is the declaration as this package writes it in Slang, and
`format` is the texture a client uploads, spelled as a GLSL layout qualifier. Of
the four fields, `semantic` is the one to match on: emitting GLSL ES pairs the
texture with a dummy sampler and names the combination itself, so the declared
name reaches the program only as a fragment. See [docs/design.md](docs/design.md).

### Select a pipeline

One tag for the behaviour and one naming the table encoding:

```python
from vcti.shader.mask import DEFINITION

assert DEFINITION.id == "mask"
assert DEFINITION.role.value == "vertex"
assert set(DEFINITION.capabilities) == {"mask", "mask-lut-r8ui"}
assert DEFINITION.slang_modules == ("mask.slang", "mask_r8ui.slang")
```

## The mask

| Value | Meaning |
|---|---|
| 0 | hidden — the vertex is collapsed outside the clip volume |
| 1 | drawn |
| anything else | drawn; the shader tests `!= 0` |

One byte per submesh — not a bit, not a word — and no stride. A second
per-submesh state
— a selection, an isolate set — is a second mask of the same shape bound in a
different pass, never a second bit here.

## 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_mask`, `mask_value`, `is_drawn`, `check_value` | build a mask, and read it the way the shader does |
| `HIDDEN`, `DRAWN`, `VALUE_MAX` | the values |
| `texel`, `rows_needed` | where a submesh's value lands, and how tall the texture is |
| `vertex_attributes`, `vertex_tables`, `vertex_uniforms` | what a build step binds |
| `ATTRIBUTE`, `TABLE`, `WIDTH` | the id, the table and the width, as specs |
| `CAPABILITY`, `ENCODING`, `ENCODING_CAPABILITY` | the tags, and the shipped texture format |

## 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 mask 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.
