vcti-shader-base 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2018-2026 Visual Collaboration Technologies Inc.
2
+ All Rights Reserved.
3
+
4
+ This software is proprietary and confidential. Unauthorized copying,
5
+ distribution, or use of this software, via any medium, is strictly
6
+ prohibited. Access is granted only to authorized VCollab developers
7
+ and individuals explicitly authorized by Visual Collaboration
8
+ Technologies Inc.
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-shader-base
3
+ Version: 1.0.0
4
+ Summary: The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies.
5
+ Author: Visual Collaboration Technologies Inc.
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-base
8
+ Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-base/blob/main/CHANGELOG.md
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Python: <3.15,>=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-cov; extra == "test"
19
+ Provides-Extra: lint
20
+ Requires-Dist: ruff; extra == "lint"
21
+ Provides-Extra: typecheck
22
+ Requires-Dist: mypy; extra == "typecheck"
23
+ Dynamic: license-file
24
+
25
+ # vcti-shader-base
26
+
27
+ The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies.
28
+
29
+ ## Overview
30
+
31
+ Shaders in this system are compiled ahead of time.
32
+
33
+ A build step turns Slang sources into GLSL ES text. Whatever draws with the
34
+ result later — a web viewer, a test harness — did not compile it and cannot
35
+ inspect it. So it has to be *told* what the shader expects:
36
+
37
+ - which buffer belongs in each vertex attribute,
38
+ - which uniforms exist and how large they are,
39
+ - which integer selects which mode.
40
+
41
+ Writing that down is what this package is for.
42
+
43
+ A **shader feature** is one piece of composable shading math. Here are some
44
+ examples:
45
+
46
+ - `deform` moves geometry,
47
+ - `fringe` colors it by bands,
48
+ - `derive` computes a quantity from a source field — scalar, vector, 6-DOF or
49
+ tensor,
50
+ - `atom-lut` culls hidden atoms.
51
+
52
+ Each ships as its own installable package. Each declares what its own math needs,
53
+ and says what it is.
54
+
55
+ `vcti-shader-base` is the vocabulary for writing exactly that declaration, and
56
+ nothing more:
57
+
58
+ - **Field specs** — `AttributeSpec` (per-vertex inputs), `UniformSpec`
59
+ (draw-constant values), `OutputSpec` (fragment outputs).
60
+ - **The definition** — `ShaderDefinition`, with `StageRole`: how a feature names
61
+ the stage it runs in, the Slang modules it ships, and the capability tags it
62
+ introduces.
63
+
64
+ Declaring a feature needs **nothing else** — no compiler, no build toolchain, no
65
+ other package.
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install vcti-shader-base
71
+ ```
72
+
73
+ Requires Python 3.12, 3.13, or 3.14. No runtime dependencies.
74
+
75
+ ### In `requirements.txt`
76
+
77
+ ```
78
+ vcti-shader-base>=1.0.0
79
+ ```
80
+
81
+ ### In `pyproject.toml` dependencies
82
+
83
+ ```toml
84
+ dependencies = [
85
+ "vcti-shader-base>=1.0.0",
86
+ ]
87
+ ```
88
+
89
+ ## Quick Start
90
+
91
+ ### Declare what the shading math needs
92
+
93
+ Consider a structural analysis. A CAE solver reports how far each node of a mesh
94
+ moves under a load, and we want to draw the deformed shape.
95
+
96
+ Every vertex carries two values — where it sits, and how far it moved. Both vary
97
+ per vertex, so both are **attributes**. We also want to exaggerate the movement,
98
+ scaling it independently in x, y and z; that factor is the same for every vertex
99
+ in the draw, so it is a **uniform**:
100
+
101
+ ```python
102
+ from vcti.shader.base import AttributeSpec, UniformSpec
103
+
104
+ inputs = (
105
+ AttributeSpec("a_position", "vec3", "coordinates"),
106
+ AttributeSpec("a_deformation", "vec3", "deformation"),
107
+ )
108
+ uniforms = (UniformSpec("u_deformScale", "vec3"),)
109
+ ```
110
+
111
+ `a_position` and `a_deformation` are the names the *shader source* uses. The
112
+ third argument is the **semantic**, and it is what makes the declaration useful
113
+ to a caller. A caller has buffers of its own — node coordinates, a displacement
114
+ field — and must know which one goes where. The name cannot answer that: it is a
115
+ shader-source detail and can be renamed. The semantic names the data instead, so
116
+ `coordinates` means mesh node positions and `deformation` means the displacement
117
+ vector.
118
+
119
+ Two optional fields are worth knowing. `array_length` stays a **number** so Python
120
+ can size a buffer, and `gl_type` joins it onto the type only where the type is
121
+ emitted:
122
+
123
+ ```python
124
+ UniformSpec("u_bandColors", "vec4", array_length=8).gl_type # 'vec4[8]'
125
+ UniformSpec("u_deformScale", "vec3").gl_type # 'vec3'
126
+ ```
127
+
128
+ `named_values` turns an integer a caller would otherwise hard-code into something
129
+ nameable:
130
+
131
+ ```python
132
+ mode = UniformSpec("u_deformMode", "int", named_values={"displacement": 0, "rotation": 1})
133
+ mode.named_values["rotation"] # 1 — the value to write
134
+ ```
135
+
136
+ A feature in the fragment stage also declares what it writes:
137
+
138
+ ```python
139
+ from vcti.shader.base import OutputSpec
140
+
141
+ outputs = (OutputSpec("fragColor", "vec4"),)
142
+ ```
143
+
144
+ ### Describe the feature itself
145
+
146
+ The specs say what the shading math needs. A `ShaderDefinition` says what the
147
+ feature *is*. Each feature constructs exactly one and exports it as `DEFINITION`:
148
+
149
+ ```python
150
+ from pathlib import Path
151
+ from vcti.shader.base import ShaderDefinition, StageRole
152
+
153
+ DEFINITION = ShaderDefinition(
154
+ id="deform",
155
+ role=StageRole.VERTEX,
156
+ capabilities=("deform3", "deform6"),
157
+ slang_modules=("deform.slang",),
158
+ slang_dir=Path(__file__).parent / "slang",
159
+ description="deform3 scaled displacement; deform6 Rodrigues rotation.",
160
+ )
161
+ ```
162
+
163
+ - **`role`** is where the feature's math runs. `VERTEX` moves geometry;
164
+ `FRAGMENT` decides color.
165
+ - **`capabilities`** are the tags this feature offers. They are opaque strings and
166
+ each feature owns its own, so adding one needs no release of this package.
167
+ - **`slang_modules`** names the Slang modules this feature publishes for a shader
168
+ to import. **`slang_dir`** says where they live — and it is the field with teeth:
169
+ the `.slang` files are installed inside the feature's own package, so only the
170
+ feature can resolve the directory, and the build passes it to the compiler as an
171
+ `import` search path. This package only records the path; it never opens it, so
172
+ confirming the files are really there is the feature's own test's job.
173
+
174
+ A feature that stops here is complete: it constructs one `ShaderDefinition`,
175
+ exports it as `DEFINITION`, and declares the specs its math needs.
176
+
177
+ ## Type Reference
178
+
179
+ | Type | Fields | Notes |
180
+ |---|---|---|
181
+ | `AttributeSpec` | `name`, `type`, `semantic` | A per-vertex input, and what its data *is* |
182
+ | `UniformSpec` | `name`, `type`, `array_length=None`, `named_values=None` | `gl_type` joins the array suffix; `named_values` only on dispatch uniforms |
183
+ | `OutputSpec` | `name`, `type` | A fragment output; no semantic, since there is nothing to bind |
184
+ | `ShaderDefinition` | `id`, `role`, `capabilities`, `slang_modules`, `slang_dir`, `description=""` | One feature's self-declaration |
185
+ | `StageRole` | `VERTEX`, `FRAGMENT` | Where the feature's math runs |
186
+
187
+ Every type is immutable and compared by value, and every one is hashable — so
188
+ specs can go in a set or a dict key, and duplicates drop out on their own.
189
+
190
+ ## Dependencies
191
+
192
+ None — the standard library covers it. A feature can declare its specs and its
193
+ definition without installing a compiler or a build toolchain behind it.
194
+
195
+ Development extras: `test` (pytest, pytest-cov), `lint` (ruff), `typecheck`
196
+ (mypy).
197
+
198
+ ## Documentation
199
+
200
+ | If you want to… | Read |
201
+ |---|---|
202
+ | Get started using the package | Quick Start above |
203
+ | Build and ship a complete feature, and avoid the pitfalls | [docs/patterns.md](docs/patterns.md) |
204
+ | Understand what these types describe and why | [docs/design.md](docs/design.md) |
205
+ | Navigate or modify the source | [docs/source-guide.md](docs/source-guide.md) |
@@ -0,0 +1,181 @@
1
+ # vcti-shader-base
2
+
3
+ The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies.
4
+
5
+ ## Overview
6
+
7
+ Shaders in this system are compiled ahead of time.
8
+
9
+ A build step turns Slang sources into GLSL ES text. Whatever draws with the
10
+ result later — a web viewer, a test harness — did not compile it and cannot
11
+ inspect it. So it has to be *told* what the shader expects:
12
+
13
+ - which buffer belongs in each vertex attribute,
14
+ - which uniforms exist and how large they are,
15
+ - which integer selects which mode.
16
+
17
+ Writing that down is what this package is for.
18
+
19
+ A **shader feature** is one piece of composable shading math. Here are some
20
+ examples:
21
+
22
+ - `deform` moves geometry,
23
+ - `fringe` colors it by bands,
24
+ - `derive` computes a quantity from a source field — scalar, vector, 6-DOF or
25
+ tensor,
26
+ - `atom-lut` culls hidden atoms.
27
+
28
+ Each ships as its own installable package. Each declares what its own math needs,
29
+ and says what it is.
30
+
31
+ `vcti-shader-base` is the vocabulary for writing exactly that declaration, and
32
+ nothing more:
33
+
34
+ - **Field specs** — `AttributeSpec` (per-vertex inputs), `UniformSpec`
35
+ (draw-constant values), `OutputSpec` (fragment outputs).
36
+ - **The definition** — `ShaderDefinition`, with `StageRole`: how a feature names
37
+ the stage it runs in, the Slang modules it ships, and the capability tags it
38
+ introduces.
39
+
40
+ Declaring a feature needs **nothing else** — no compiler, no build toolchain, no
41
+ other package.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install vcti-shader-base
47
+ ```
48
+
49
+ Requires Python 3.12, 3.13, or 3.14. No runtime dependencies.
50
+
51
+ ### In `requirements.txt`
52
+
53
+ ```
54
+ vcti-shader-base>=1.0.0
55
+ ```
56
+
57
+ ### In `pyproject.toml` dependencies
58
+
59
+ ```toml
60
+ dependencies = [
61
+ "vcti-shader-base>=1.0.0",
62
+ ]
63
+ ```
64
+
65
+ ## Quick Start
66
+
67
+ ### Declare what the shading math needs
68
+
69
+ Consider a structural analysis. A CAE solver reports how far each node of a mesh
70
+ moves under a load, and we want to draw the deformed shape.
71
+
72
+ Every vertex carries two values — where it sits, and how far it moved. Both vary
73
+ per vertex, so both are **attributes**. We also want to exaggerate the movement,
74
+ scaling it independently in x, y and z; that factor is the same for every vertex
75
+ in the draw, so it is a **uniform**:
76
+
77
+ ```python
78
+ from vcti.shader.base import AttributeSpec, UniformSpec
79
+
80
+ inputs = (
81
+ AttributeSpec("a_position", "vec3", "coordinates"),
82
+ AttributeSpec("a_deformation", "vec3", "deformation"),
83
+ )
84
+ uniforms = (UniformSpec("u_deformScale", "vec3"),)
85
+ ```
86
+
87
+ `a_position` and `a_deformation` are the names the *shader source* uses. The
88
+ third argument is the **semantic**, and it is what makes the declaration useful
89
+ to a caller. A caller has buffers of its own — node coordinates, a displacement
90
+ field — and must know which one goes where. The name cannot answer that: it is a
91
+ shader-source detail and can be renamed. The semantic names the data instead, so
92
+ `coordinates` means mesh node positions and `deformation` means the displacement
93
+ vector.
94
+
95
+ Two optional fields are worth knowing. `array_length` stays a **number** so Python
96
+ can size a buffer, and `gl_type` joins it onto the type only where the type is
97
+ emitted:
98
+
99
+ ```python
100
+ UniformSpec("u_bandColors", "vec4", array_length=8).gl_type # 'vec4[8]'
101
+ UniformSpec("u_deformScale", "vec3").gl_type # 'vec3'
102
+ ```
103
+
104
+ `named_values` turns an integer a caller would otherwise hard-code into something
105
+ nameable:
106
+
107
+ ```python
108
+ mode = UniformSpec("u_deformMode", "int", named_values={"displacement": 0, "rotation": 1})
109
+ mode.named_values["rotation"] # 1 — the value to write
110
+ ```
111
+
112
+ A feature in the fragment stage also declares what it writes:
113
+
114
+ ```python
115
+ from vcti.shader.base import OutputSpec
116
+
117
+ outputs = (OutputSpec("fragColor", "vec4"),)
118
+ ```
119
+
120
+ ### Describe the feature itself
121
+
122
+ The specs say what the shading math needs. A `ShaderDefinition` says what the
123
+ feature *is*. Each feature constructs exactly one and exports it as `DEFINITION`:
124
+
125
+ ```python
126
+ from pathlib import Path
127
+ from vcti.shader.base import ShaderDefinition, StageRole
128
+
129
+ DEFINITION = ShaderDefinition(
130
+ id="deform",
131
+ role=StageRole.VERTEX,
132
+ capabilities=("deform3", "deform6"),
133
+ slang_modules=("deform.slang",),
134
+ slang_dir=Path(__file__).parent / "slang",
135
+ description="deform3 scaled displacement; deform6 Rodrigues rotation.",
136
+ )
137
+ ```
138
+
139
+ - **`role`** is where the feature's math runs. `VERTEX` moves geometry;
140
+ `FRAGMENT` decides color.
141
+ - **`capabilities`** are the tags this feature offers. They are opaque strings and
142
+ each feature owns its own, so adding one needs no release of this package.
143
+ - **`slang_modules`** names the Slang modules this feature publishes for a shader
144
+ to import. **`slang_dir`** says where they live — and it is the field with teeth:
145
+ the `.slang` files are installed inside the feature's own package, so only the
146
+ feature can resolve the directory, and the build passes it to the compiler as an
147
+ `import` search path. This package only records the path; it never opens it, so
148
+ confirming the files are really there is the feature's own test's job.
149
+
150
+ A feature that stops here is complete: it constructs one `ShaderDefinition`,
151
+ exports it as `DEFINITION`, and declares the specs its math needs.
152
+
153
+ ## Type Reference
154
+
155
+ | Type | Fields | Notes |
156
+ |---|---|---|
157
+ | `AttributeSpec` | `name`, `type`, `semantic` | A per-vertex input, and what its data *is* |
158
+ | `UniformSpec` | `name`, `type`, `array_length=None`, `named_values=None` | `gl_type` joins the array suffix; `named_values` only on dispatch uniforms |
159
+ | `OutputSpec` | `name`, `type` | A fragment output; no semantic, since there is nothing to bind |
160
+ | `ShaderDefinition` | `id`, `role`, `capabilities`, `slang_modules`, `slang_dir`, `description=""` | One feature's self-declaration |
161
+ | `StageRole` | `VERTEX`, `FRAGMENT` | Where the feature's math runs |
162
+
163
+ Every type is immutable and compared by value, and every one is hashable — so
164
+ specs can go in a set or a dict key, and duplicates drop out on their own.
165
+
166
+ ## Dependencies
167
+
168
+ None — the standard library covers it. A feature can declare its specs and its
169
+ definition without installing a compiler or a build toolchain behind it.
170
+
171
+ Development extras: `test` (pytest, pytest-cov), `lint` (ruff), `typecheck`
172
+ (mypy).
173
+
174
+ ## Documentation
175
+
176
+ | If you want to… | Read |
177
+ |---|---|
178
+ | Get started using the package | Quick Start above |
179
+ | Build and ship a complete feature, and avoid the pitfalls | [docs/patterns.md](docs/patterns.md) |
180
+ | Understand what these types describe and why | [docs/design.md](docs/design.md) |
181
+ | Navigate or modify the source | [docs/source-guide.md](docs/source-guide.md) |
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "vcti-shader-base"
7
+ version = "1.0.0"
8
+ description = "The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies."
9
+ readme = "README.md"
10
+ authors = [
11
+ {name = "Visual Collaboration Technologies Inc."}
12
+ ]
13
+ license = "LicenseRef-Proprietary"
14
+ license-files = ["LICENSE"]
15
+ classifiers = [
16
+ "Operating System :: OS Independent",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ ]
21
+ requires-python = ">=3.12,<3.15"
22
+ dependencies = []
23
+
24
+ [project.urls]
25
+ Repository = "https://github.com/vcollab/vcti-python-shader-base"
26
+ Changelog = "https://github.com/vcollab/vcti-python-shader-base/blob/main/CHANGELOG.md"
27
+
28
+ [tool.setuptools.packages.find]
29
+ where = ["src"]
30
+ include = ["vcti.shader.base", "vcti.shader.base.*"]
31
+
32
+ [tool.setuptools.package-data]
33
+ "vcti.shader.base" = ["py.typed"]
34
+
35
+ [project.optional-dependencies]
36
+ test = ["pytest", "pytest-cov"]
37
+ lint = ["ruff"]
38
+ typecheck = ["mypy"]
39
+
40
+ [tool.pytest.ini_options]
41
+ addopts = "--cov=vcti.shader.base --cov-report=term-missing --cov-fail-under=95"
42
+
43
+ [tool.mypy]
44
+ python_version = "3.12"
45
+ strict = true
46
+ files = ["src"]
47
+ namespace_packages = true
48
+ explicit_package_bases = true
49
+ mypy_path = ["src"]
50
+
51
+ [tool.coverage.run]
52
+ branch = true
53
+
54
+ [tool.coverage.report]
55
+ exclude_also = [
56
+ "raise NotImplementedError",
57
+ "if TYPE_CHECKING:",
58
+ "if __name__ == .__main__.:",
59
+ "@(abc\\.)?abstractmethod",
60
+ "\\.\\.\\.",
61
+ ]
62
+
63
+ [tool.ruff]
64
+ target-version = "py312"
65
+ line-length = 99
66
+
67
+ [tool.ruff.lint]
68
+ select = ["E", "F", "W", "I", "UP"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,27 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """vcti.shader.base — the vocabulary a shader feature declares itself with.
4
+
5
+ The field specs a feature declares — :class:`AttributeSpec`, :class:`UniformSpec`,
6
+ :class:`OutputSpec` — and the :class:`ShaderDefinition` record (with
7
+ :class:`StageRole`) by which a feature names its stage, Slang modules, and
8
+ capability tags. Types, not behavior, and zero runtime dependencies: a
9
+ shader feature (deform, fringe, derive, atom-lut, …) declares itself against this
10
+ alone, with nothing about how shaders are built in view. See ``docs/design.md``.
11
+ """
12
+
13
+ from importlib.metadata import version
14
+
15
+ from .definition import ShaderDefinition, StageRole
16
+ from .specs import AttributeSpec, OutputSpec, UniformSpec
17
+
18
+ __version__ = version("vcti-shader-base")
19
+
20
+ __all__ = [
21
+ "AttributeSpec",
22
+ "OutputSpec",
23
+ "ShaderDefinition",
24
+ "StageRole",
25
+ "UniformSpec",
26
+ "__version__",
27
+ ]
@@ -0,0 +1,60 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """The shader-feature definition: how one feature declares what it is.
4
+
5
+ A **shader feature** — fringe, deform, derive, atom-lut, and any future one — is
6
+ one piece of composable shading math, shipped as its own installable package. Each
7
+ declares a single :class:`ShaderDefinition` naming the stage its math runs in, the
8
+ Slang modules it ships, and the capability tags it offers. This is the pure-data
9
+ record a feature constructs and exports as ``DEFINITION``, so a feature declares
10
+ itself against this package alone.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from dataclasses import dataclass
16
+ from enum import StrEnum
17
+ from pathlib import Path
18
+
19
+
20
+ class StageRole(StrEnum):
21
+ """Which of the two shader stages a feature's math runs in.
22
+
23
+ The values are a serialization contract: they reach build artifacts and the
24
+ consumers that read them back, so renaming a member is source-compatible but
25
+ changing a value is a breaking change.
26
+ """
27
+
28
+ VERTEX = "vertex" # transforms gl_Position (deform, atom-lut)
29
+ FRAGMENT = "fragment" # produces the fragment output (derive, fringe)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ShaderDefinition:
34
+ """One shader feature: identity, stage, capability tags, and Slang modules.
35
+
36
+ ``id`` names the feature for logs and diagnostics. ``capabilities`` are the
37
+ (open) tag strings this feature offers; nothing here interprets them, which is
38
+ why they are plain strings and not a closed enum — introducing a tag needs no
39
+ release of this package.
40
+
41
+ ``slang_modules`` names the Slang modules this feature publishes for a shader
42
+ to import; ``slang_dir`` is the directory holding them. A build step adds that
43
+ directory to the compiler's ``import`` search path so a shader can resolve
44
+ modules living inside a feature's own installed package — which is also why
45
+ only the feature can supply it. The path is only recorded here: this package
46
+ never opens or resolves it, so a directory that does not exist is accepted
47
+ without complaint, and checking that the modules are really shipped is a
48
+ feature's own test's job.
49
+
50
+ ``description`` is the only defaulted field, which fixes the argument order:
51
+ a new required field must go before it, or the class raises ``TypeError`` at
52
+ import for a non-default argument following a default one.
53
+ """
54
+
55
+ id: str
56
+ role: StageRole
57
+ capabilities: tuple[str, ...]
58
+ slang_modules: tuple[str, ...]
59
+ slang_dir: Path
60
+ description: str = ""
File without changes
@@ -0,0 +1,86 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """The field specs a shader feature declares: attributes, uniforms, and outputs.
4
+
5
+ A shader feature describes the shading math it contributes as a set of per-vertex
6
+ inputs (:class:`AttributeSpec`), draw-constant values (:class:`UniformSpec`), and
7
+ fragment outputs (:class:`OutputSpec`). These are pure data types with no
8
+ behavior beyond describing a shape; nothing here validates a GLSL type name, an
9
+ array length, or a semantic, because a feature declares its specs in isolation —
10
+ before any complete shader exists — so a check that needs to see more than one
11
+ feature cannot be answered here.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import Mapping
17
+ from dataclasses import dataclass
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class AttributeSpec:
22
+ """A vertex attribute input: name, GLSL type, and its data semantic.
23
+
24
+ ``name`` is the attribute's identifier in the shader source; ``semantic``
25
+ names what the data *is* (``coordinates``, ``deformation``, ``result.0``), so
26
+ a caller knows which of its own buffers belongs here. The semantic vocabulary
27
+ belongs to the features — nothing here interprets the string.
28
+ """
29
+
30
+ name: str
31
+ type: str
32
+ semantic: str
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class UniformSpec:
37
+ """A uniform: name, base GLSL type, optional array length and named values.
38
+
39
+ ``array_length`` is held as a number rather than baked into ``type`` so Python
40
+ can size a buffer or check that a value fits; :attr:`gl_type` joins the two
41
+ only where the type is emitted. ``named_values`` is the name-to-number map of a
42
+ *dispatch* uniform — an ``int`` selecting a branch inside the shader — so a
43
+ caller can write the value by name instead of hard-coding it. It is ``None`` on
44
+ every other uniform.
45
+ """
46
+
47
+ name: str
48
+ type: str
49
+ array_length: int | None = None
50
+ named_values: Mapping[str, int] | None = None
51
+
52
+ @property
53
+ def gl_type(self) -> str:
54
+ """The declared type, with an array suffix when the uniform is an array."""
55
+ if self.array_length is None:
56
+ return self.type
57
+ return f"{self.type}[{self.array_length}]"
58
+
59
+ def __hash__(self) -> int:
60
+ """Hash by value, folding ``named_values`` into a hashable form.
61
+
62
+ ``frozen=True`` promises hashability, but the generated ``__hash__``
63
+ hashes the fields as-is and ``named_values`` holds a mapping — so a uniform
64
+ that declares one raised ``TypeError`` the moment anything put it in a set,
65
+ de-duplicated a group of specs, or used one as a dict key. The field stays a
66
+ ``Mapping`` (callers pass plain dicts); only the hash normalizes, sorted so
67
+ that two equal maps written in different orders hash alike.
68
+
69
+ A field added to this class must be added here too, or two different
70
+ uniforms collide in a set; the tests pin the field list so the omission
71
+ fails rather than passing silently.
72
+ """
73
+ values = None if self.named_values is None else tuple(sorted(self.named_values.items()))
74
+ return hash((self.name, self.type, self.array_length, values))
75
+
76
+
77
+ @dataclass(frozen=True)
78
+ class OutputSpec:
79
+ """A fragment output: name and GLSL type.
80
+
81
+ Separate from :class:`AttributeSpec` because it carries no semantic: there is
82
+ nothing for a caller to bind, only a result the shader writes.
83
+ """
84
+
85
+ name: str
86
+ type: str
@@ -0,0 +1,205 @@
1
+ Metadata-Version: 2.4
2
+ Name: vcti-shader-base
3
+ Version: 1.0.0
4
+ Summary: The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies.
5
+ Author: Visual Collaboration Technologies Inc.
6
+ License-Expression: LicenseRef-Proprietary
7
+ Project-URL: Repository, https://github.com/vcollab/vcti-python-shader-base
8
+ Project-URL: Changelog, https://github.com/vcollab/vcti-python-shader-base/blob/main/CHANGELOG.md
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python :: 3.12
11
+ Classifier: Programming Language :: Python :: 3.13
12
+ Classifier: Programming Language :: Python :: 3.14
13
+ Requires-Python: <3.15,>=3.12
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == "test"
18
+ Requires-Dist: pytest-cov; extra == "test"
19
+ Provides-Extra: lint
20
+ Requires-Dist: ruff; extra == "lint"
21
+ Provides-Extra: typecheck
22
+ Requires-Dist: mypy; extra == "typecheck"
23
+ Dynamic: license-file
24
+
25
+ # vcti-shader-base
26
+
27
+ The vocabulary a shader feature declares itself with: attribute/uniform/output specs and the ShaderDefinition record. Zero dependencies.
28
+
29
+ ## Overview
30
+
31
+ Shaders in this system are compiled ahead of time.
32
+
33
+ A build step turns Slang sources into GLSL ES text. Whatever draws with the
34
+ result later — a web viewer, a test harness — did not compile it and cannot
35
+ inspect it. So it has to be *told* what the shader expects:
36
+
37
+ - which buffer belongs in each vertex attribute,
38
+ - which uniforms exist and how large they are,
39
+ - which integer selects which mode.
40
+
41
+ Writing that down is what this package is for.
42
+
43
+ A **shader feature** is one piece of composable shading math. Here are some
44
+ examples:
45
+
46
+ - `deform` moves geometry,
47
+ - `fringe` colors it by bands,
48
+ - `derive` computes a quantity from a source field — scalar, vector, 6-DOF or
49
+ tensor,
50
+ - `atom-lut` culls hidden atoms.
51
+
52
+ Each ships as its own installable package. Each declares what its own math needs,
53
+ and says what it is.
54
+
55
+ `vcti-shader-base` is the vocabulary for writing exactly that declaration, and
56
+ nothing more:
57
+
58
+ - **Field specs** — `AttributeSpec` (per-vertex inputs), `UniformSpec`
59
+ (draw-constant values), `OutputSpec` (fragment outputs).
60
+ - **The definition** — `ShaderDefinition`, with `StageRole`: how a feature names
61
+ the stage it runs in, the Slang modules it ships, and the capability tags it
62
+ introduces.
63
+
64
+ Declaring a feature needs **nothing else** — no compiler, no build toolchain, no
65
+ other package.
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install vcti-shader-base
71
+ ```
72
+
73
+ Requires Python 3.12, 3.13, or 3.14. No runtime dependencies.
74
+
75
+ ### In `requirements.txt`
76
+
77
+ ```
78
+ vcti-shader-base>=1.0.0
79
+ ```
80
+
81
+ ### In `pyproject.toml` dependencies
82
+
83
+ ```toml
84
+ dependencies = [
85
+ "vcti-shader-base>=1.0.0",
86
+ ]
87
+ ```
88
+
89
+ ## Quick Start
90
+
91
+ ### Declare what the shading math needs
92
+
93
+ Consider a structural analysis. A CAE solver reports how far each node of a mesh
94
+ moves under a load, and we want to draw the deformed shape.
95
+
96
+ Every vertex carries two values — where it sits, and how far it moved. Both vary
97
+ per vertex, so both are **attributes**. We also want to exaggerate the movement,
98
+ scaling it independently in x, y and z; that factor is the same for every vertex
99
+ in the draw, so it is a **uniform**:
100
+
101
+ ```python
102
+ from vcti.shader.base import AttributeSpec, UniformSpec
103
+
104
+ inputs = (
105
+ AttributeSpec("a_position", "vec3", "coordinates"),
106
+ AttributeSpec("a_deformation", "vec3", "deformation"),
107
+ )
108
+ uniforms = (UniformSpec("u_deformScale", "vec3"),)
109
+ ```
110
+
111
+ `a_position` and `a_deformation` are the names the *shader source* uses. The
112
+ third argument is the **semantic**, and it is what makes the declaration useful
113
+ to a caller. A caller has buffers of its own — node coordinates, a displacement
114
+ field — and must know which one goes where. The name cannot answer that: it is a
115
+ shader-source detail and can be renamed. The semantic names the data instead, so
116
+ `coordinates` means mesh node positions and `deformation` means the displacement
117
+ vector.
118
+
119
+ Two optional fields are worth knowing. `array_length` stays a **number** so Python
120
+ can size a buffer, and `gl_type` joins it onto the type only where the type is
121
+ emitted:
122
+
123
+ ```python
124
+ UniformSpec("u_bandColors", "vec4", array_length=8).gl_type # 'vec4[8]'
125
+ UniformSpec("u_deformScale", "vec3").gl_type # 'vec3'
126
+ ```
127
+
128
+ `named_values` turns an integer a caller would otherwise hard-code into something
129
+ nameable:
130
+
131
+ ```python
132
+ mode = UniformSpec("u_deformMode", "int", named_values={"displacement": 0, "rotation": 1})
133
+ mode.named_values["rotation"] # 1 — the value to write
134
+ ```
135
+
136
+ A feature in the fragment stage also declares what it writes:
137
+
138
+ ```python
139
+ from vcti.shader.base import OutputSpec
140
+
141
+ outputs = (OutputSpec("fragColor", "vec4"),)
142
+ ```
143
+
144
+ ### Describe the feature itself
145
+
146
+ The specs say what the shading math needs. A `ShaderDefinition` says what the
147
+ feature *is*. Each feature constructs exactly one and exports it as `DEFINITION`:
148
+
149
+ ```python
150
+ from pathlib import Path
151
+ from vcti.shader.base import ShaderDefinition, StageRole
152
+
153
+ DEFINITION = ShaderDefinition(
154
+ id="deform",
155
+ role=StageRole.VERTEX,
156
+ capabilities=("deform3", "deform6"),
157
+ slang_modules=("deform.slang",),
158
+ slang_dir=Path(__file__).parent / "slang",
159
+ description="deform3 scaled displacement; deform6 Rodrigues rotation.",
160
+ )
161
+ ```
162
+
163
+ - **`role`** is where the feature's math runs. `VERTEX` moves geometry;
164
+ `FRAGMENT` decides color.
165
+ - **`capabilities`** are the tags this feature offers. They are opaque strings and
166
+ each feature owns its own, so adding one needs no release of this package.
167
+ - **`slang_modules`** names the Slang modules this feature publishes for a shader
168
+ to import. **`slang_dir`** says where they live — and it is the field with teeth:
169
+ the `.slang` files are installed inside the feature's own package, so only the
170
+ feature can resolve the directory, and the build passes it to the compiler as an
171
+ `import` search path. This package only records the path; it never opens it, so
172
+ confirming the files are really there is the feature's own test's job.
173
+
174
+ A feature that stops here is complete: it constructs one `ShaderDefinition`,
175
+ exports it as `DEFINITION`, and declares the specs its math needs.
176
+
177
+ ## Type Reference
178
+
179
+ | Type | Fields | Notes |
180
+ |---|---|---|
181
+ | `AttributeSpec` | `name`, `type`, `semantic` | A per-vertex input, and what its data *is* |
182
+ | `UniformSpec` | `name`, `type`, `array_length=None`, `named_values=None` | `gl_type` joins the array suffix; `named_values` only on dispatch uniforms |
183
+ | `OutputSpec` | `name`, `type` | A fragment output; no semantic, since there is nothing to bind |
184
+ | `ShaderDefinition` | `id`, `role`, `capabilities`, `slang_modules`, `slang_dir`, `description=""` | One feature's self-declaration |
185
+ | `StageRole` | `VERTEX`, `FRAGMENT` | Where the feature's math runs |
186
+
187
+ Every type is immutable and compared by value, and every one is hashable — so
188
+ specs can go in a set or a dict key, and duplicates drop out on their own.
189
+
190
+ ## Dependencies
191
+
192
+ None — the standard library covers it. A feature can declare its specs and its
193
+ definition without installing a compiler or a build toolchain behind it.
194
+
195
+ Development extras: `test` (pytest, pytest-cov), `lint` (ruff), `typecheck`
196
+ (mypy).
197
+
198
+ ## Documentation
199
+
200
+ | If you want to… | Read |
201
+ |---|---|
202
+ | Get started using the package | Quick Start above |
203
+ | Build and ship a complete feature, and avoid the pitfalls | [docs/patterns.md](docs/patterns.md) |
204
+ | Understand what these types describe and why | [docs/design.md](docs/design.md) |
205
+ | Navigate or modify the source | [docs/source-guide.md](docs/source-guide.md) |
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/vcti/shader/base/__init__.py
5
+ src/vcti/shader/base/definition.py
6
+ src/vcti/shader/base/py.typed
7
+ src/vcti/shader/base/specs.py
8
+ src/vcti_shader_base.egg-info/PKG-INFO
9
+ src/vcti_shader_base.egg-info/SOURCES.txt
10
+ src/vcti_shader_base.egg-info/dependency_links.txt
11
+ src/vcti_shader_base.egg-info/requires.txt
12
+ src/vcti_shader_base.egg-info/top_level.txt
13
+ tests/test_definition.py
14
+ tests/test_specs.py
15
+ tests/test_version.py
@@ -0,0 +1,10 @@
1
+
2
+ [lint]
3
+ ruff
4
+
5
+ [test]
6
+ pytest
7
+ pytest-cov
8
+
9
+ [typecheck]
10
+ mypy
@@ -0,0 +1,78 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for the ShaderDefinition record and its stage-role enum."""
4
+
5
+ import dataclasses
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from vcti.shader.base import ShaderDefinition, StageRole
11
+
12
+
13
+ class TestStageRole:
14
+ # The values are serialized, so they are pinned individually, not as a set.
15
+ def test_values(self):
16
+ assert StageRole.VERTEX == "vertex"
17
+ assert StageRole.FRAGMENT == "fragment"
18
+
19
+ def test_members(self):
20
+ assert list(StageRole) == [StageRole.VERTEX, StageRole.FRAGMENT]
21
+
22
+
23
+ class TestShaderDefinition:
24
+ def test_declares_a_feature(self):
25
+ definition = ShaderDefinition(
26
+ id="fringe",
27
+ role=StageRole.FRAGMENT,
28
+ capabilities=("fringe",),
29
+ slang_modules=("colormap.slang",),
30
+ slang_dir=Path("slang"),
31
+ description="Band-list fringe colormap.",
32
+ )
33
+ assert definition.id == "fringe"
34
+ assert definition.role is StageRole.FRAGMENT
35
+ assert definition.capabilities == ("fringe",)
36
+ assert definition.slang_modules == ("colormap.slang",)
37
+ assert definition.slang_dir == Path("slang")
38
+ assert definition.description == "Band-list fringe colormap."
39
+
40
+ def test_description_defaults_empty(self):
41
+ definition = ShaderDefinition(
42
+ "deform",
43
+ StageRole.VERTEX,
44
+ ("deform3",),
45
+ ("deform.slang",),
46
+ Path("."),
47
+ )
48
+ assert definition.description == ""
49
+
50
+ def test_description_stays_the_last_field(self):
51
+ # It is the only defaulted field, so a new required field placed after it
52
+ # raises TypeError when the class is created — at import.
53
+ assert [field.name for field in dataclasses.fields(ShaderDefinition)][-1] == "description"
54
+
55
+ def test_frozen(self):
56
+ definition = ShaderDefinition(
57
+ "deform",
58
+ StageRole.VERTEX,
59
+ ("deform3",),
60
+ ("deform.slang",),
61
+ Path("."),
62
+ )
63
+ with pytest.raises(dataclasses.FrozenInstanceError):
64
+ definition.id = "other" # type: ignore[misc]
65
+
66
+ def test_hashable_and_compared_by_value(self):
67
+ # A builder collects definitions into a set and de-duplicates slang_dirs.
68
+ def make():
69
+ return ShaderDefinition(
70
+ "deform",
71
+ StageRole.VERTEX,
72
+ ("deform3", "deform6"),
73
+ ("deform.slang",),
74
+ Path("slang"),
75
+ )
76
+
77
+ assert make() == make()
78
+ assert len({make(), make()}) == 1
@@ -0,0 +1,98 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Tests for the field spec types."""
4
+
5
+ import dataclasses
6
+
7
+ import pytest
8
+
9
+ from vcti.shader.base import AttributeSpec, OutputSpec, UniformSpec
10
+
11
+
12
+ class TestUniformSpec:
13
+ def test_gl_type_plain(self):
14
+ assert UniformSpec("u_scale", "float").gl_type == "float"
15
+
16
+ def test_gl_type_array(self):
17
+ assert UniformSpec("u_bandBounds", "vec2", array_length=72).gl_type == "vec2[72]"
18
+
19
+ def test_named_values_is_optional(self):
20
+ assert UniformSpec("u_x", "int").named_values is None
21
+ assert UniformSpec("u_mode", "int", named_values={"a": 0}).named_values == {"a": 0}
22
+
23
+ def test_hashable_without_named_values(self):
24
+ spec = UniformSpec("u_scale", "float")
25
+ assert spec in {spec}
26
+
27
+ def test_hashable_with_named_values(self):
28
+ # A dispatch uniform holds a dict; it must still go in a set.
29
+ spec = UniformSpec("u_mode", "int", named_values={"a": 0, "b": 1})
30
+ assert spec in {spec}
31
+
32
+ def test_equal_named_values_hash_alike_regardless_of_order(self):
33
+ a = UniformSpec("u_mode", "int", named_values={"a": 0, "b": 1})
34
+ b = UniformSpec("u_mode", "int", named_values={"b": 1, "a": 0})
35
+ assert a == b
36
+ assert hash(a) == hash(b)
37
+
38
+ def test_differing_named_values_do_not_collide(self):
39
+ a = UniformSpec("u_mode", "int", named_values={"a": 0})
40
+ b = UniformSpec("u_mode", "int", named_values={"a": 1})
41
+ assert a != b
42
+ assert len({a, b}) == 2
43
+
44
+ def test_hash_covers_every_field(self):
45
+ # __hash__ is hand-written, so a new field is silently left out of it and
46
+ # two different uniforms start colliding in a set. The field list is pinned
47
+ # here so adding one fails until __hash__ is extended to match.
48
+ assert [field.name for field in dataclasses.fields(UniformSpec)] == [
49
+ "name",
50
+ "type",
51
+ "array_length",
52
+ "named_values",
53
+ ]
54
+
55
+ def test_array_length_is_part_of_identity(self):
56
+ assert UniformSpec("u_bands", "vec2", array_length=8) != UniformSpec(
57
+ "u_bands", "vec2", array_length=16
58
+ )
59
+
60
+ def test_frozen(self):
61
+ spec = UniformSpec("u_scale", "float")
62
+ with pytest.raises(dataclasses.FrozenInstanceError):
63
+ spec.name = "x" # type: ignore[misc]
64
+
65
+
66
+ class TestAttributeSpec:
67
+ def test_carries_the_semantic(self):
68
+ attr = AttributeSpec("a_position", "vec3", "coordinates")
69
+ assert (attr.name, attr.type, attr.semantic) == ("a_position", "vec3", "coordinates")
70
+
71
+ def test_semantic_is_part_of_identity(self):
72
+ # The name is a shader-source detail; the semantic names the data, so two
73
+ # attributes differing only in semantic are different declarations.
74
+ a = AttributeSpec("a_result0", "vec3", "result.0")
75
+ b = AttributeSpec("a_result0", "vec3", "result.1")
76
+ assert a != b
77
+ assert len({a, b}) == 2
78
+
79
+ def test_frozen(self):
80
+ attr = AttributeSpec("a_position", "vec3", "coordinates")
81
+ with pytest.raises(dataclasses.FrozenInstanceError):
82
+ attr.name = "x" # type: ignore[misc]
83
+
84
+
85
+ class TestOutputSpec:
86
+ def test_fields(self):
87
+ out = OutputSpec("fragColor", "vec4")
88
+ assert out.name == "fragColor"
89
+ assert out.type == "vec4"
90
+
91
+ def test_hashable_and_compared_by_value(self):
92
+ assert OutputSpec("fragColor", "vec4") == OutputSpec("fragColor", "vec4")
93
+ assert len({OutputSpec("fragColor", "vec4"), OutputSpec("fragColor", "vec4")}) == 1
94
+
95
+ def test_frozen(self):
96
+ out = OutputSpec("fragColor", "vec4")
97
+ with pytest.raises(dataclasses.FrozenInstanceError):
98
+ out.type = "vec3" # type: ignore[misc]
@@ -0,0 +1,32 @@
1
+ # Copyright Visual Collaboration Technologies Inc. All Rights Reserved.
2
+ # See LICENSE for details.
3
+ """Version and public-surface tests for vcti-shader-base."""
4
+
5
+ import re
6
+
7
+ import vcti.shader.base
8
+
9
+
10
+ class TestVersion:
11
+ def test_version_exists(self):
12
+ assert hasattr(vcti.shader.base, "__version__")
13
+
14
+ def test_version_is_valid_semver(self):
15
+ assert re.match(r"^\d+\.\d+\.\d+", vcti.shader.base.__version__)
16
+
17
+
18
+ class TestPublicSurface:
19
+ def test_all_is_the_documented_vocabulary(self):
20
+ # The package is a vocabulary: what it exports *is* its contract.
21
+ assert vcti.shader.base.__all__ == [
22
+ "AttributeSpec",
23
+ "OutputSpec",
24
+ "ShaderDefinition",
25
+ "StageRole",
26
+ "UniformSpec",
27
+ "__version__",
28
+ ]
29
+
30
+ def test_every_name_in_all_is_importable(self):
31
+ for name in vcti.shader.base.__all__:
32
+ assert hasattr(vcti.shader.base, name), name