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.
- vcti_shader_base-1.0.0/LICENSE +8 -0
- vcti_shader_base-1.0.0/PKG-INFO +205 -0
- vcti_shader_base-1.0.0/README.md +181 -0
- vcti_shader_base-1.0.0/pyproject.toml +68 -0
- vcti_shader_base-1.0.0/setup.cfg +4 -0
- vcti_shader_base-1.0.0/src/vcti/shader/base/__init__.py +27 -0
- vcti_shader_base-1.0.0/src/vcti/shader/base/definition.py +60 -0
- vcti_shader_base-1.0.0/src/vcti/shader/base/py.typed +0 -0
- vcti_shader_base-1.0.0/src/vcti/shader/base/specs.py +86 -0
- vcti_shader_base-1.0.0/src/vcti_shader_base.egg-info/PKG-INFO +205 -0
- vcti_shader_base-1.0.0/src/vcti_shader_base.egg-info/SOURCES.txt +15 -0
- vcti_shader_base-1.0.0/src/vcti_shader_base.egg-info/dependency_links.txt +1 -0
- vcti_shader_base-1.0.0/src/vcti_shader_base.egg-info/requires.txt +10 -0
- vcti_shader_base-1.0.0/src/vcti_shader_base.egg-info/top_level.txt +1 -0
- vcti_shader_base-1.0.0/tests/test_definition.py +78 -0
- vcti_shader_base-1.0.0/tests/test_specs.py +98 -0
- vcti_shader_base-1.0.0/tests/test_version.py +32 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
vcti
|
|
@@ -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
|