Skip to content

Commit 70b812a

Browse files
committed
BackendCpp: C++ source-generation backend for OSL shader groups
Add a BackendCpp execution path that generates human-readable, compilable C++ from the post-optimized shader graph, as an alternative to the LLVM JIT. The direct benefit is for debugging and understanding post-optimized shader by looking at its C++ equivalent rather than huge a pile of oso. Possible secondary/future benefits (with a little more work): - Speed, if DSO loading is faster than optimize/JIT. - Pre-compiled shader networks for use in a renderer that needs a fixed set of shaders and no runtime JIT. - Distributing proprietary shaders as binaries. But the REAL purpose is as a stepping stone to translating to other languages like Metal or GLSL. I envision the mechanism to simply be to subclass BackendCpp, inheriting all the parts that are common to all C++-like languages, and using virtual methods as the customization points for language-specific differences. (I don't claim that these customization points are correct now; I expect significant refactoring needs to be apparent when we add the next language.) Anyway, so as for what's here: Pipeline, controlled by the escalating `debug_output_cpp` ShadingSystem attribute (and the `OSL_DEBUG_OUTPUT_CPP` env var): 1 - generate a self-contained `group-cpp-<name>.cpp` 2 - also shell out to compile it to a DSO 3 - also load the DSO, verify its ABI, and execute it instead of the JIT (running BackendLLVM's layout pass only, skipping full JIT codegen) Generated output: a typed `GroupData` struct mirroring the BackendLLVM layout (layer-run flags, userdata, connected/output params), one `void layer_N(ShaderGlobals*, GroupData*, ...)` function per active layer, and a `RunLLVMGroupFunc`-compatible group entry. Every file includes the new internal `osl_cpp_runtime.h` (ABI version + `extern "C"` osl_* decls) and exports `osl_cpp_abi_version()`, checked at load. The `lang_*` virtual interface keeps the language tokens overridable for future backends. Op coverage is complete: arithmetic/comparison/math emit direct C++; runtime-service ops (noise, texture, closures, transforms, pointcloud, getattribute/getmatrix/gettextureinfo, splines, regex, messages, ...) call osl_* functions. Control flow, arrays/components, structs (incl. color2/4, vector2/4), and Dual2 derivatives (scalar and triple) are all generated and verified against the JIT. Testing is opt-out and automatic: the `OSL_TEST_CPP_BACKEND` CMake option (ON in the linux-vfx2026 and macos26-arm CI variants) builds a per-group DSO and runs every testshade/testrender test through the C++ path at both opt levels. The full eligible suite passes, matching JIT output (with the documented opt-outs: --entry/layers-entry, the backend-cpp fixture itself, OptiX/GPU, and batched-regression harnesses). Cross-platform notes: - osl_* shadeops are exported from liboslexec for generated DSOs to resolve. On Linux this required listing osl_* under `global:` in hidesymbols.map (the version script otherwise localized them out of the dynamic symbol table); they are marked INTERNAL/UNSTABLE. - OSL_CPP_ABI_VERSION folds in the OSL major/minor version so minor releases are link-incompatible automatically (the DSOs are ephemeral; the check only guards against misuse). - MSVC-clean (OSL::popcount instead of a GCC/Clang builtin). - Array copy of mismatched lengths copies min(dst,src) elements, matching BackendLLVM and avoiding an out-of-bounds source read. Spec, plan, research, data-model, and tasks under docs/dev/specs/002-backend-cpp/. Assisted-by: Claude Code / claude-sonnet-4-6 and claude-opus-4.8 Signed-off-by: Larry Gritz <lg@larrygritz.com>
1 parent ac508f9 commit 70b812a

41 files changed

Lines changed: 8181 additions & 35 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/ci.yml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -207,7 +207,9 @@ jobs:
207207
python_ver: "3.13"
208208
simd: avx2,f16c
209209
batched: b8_AVX2
210-
setenvs: export CTEST_EXCLUSIONS="broken|python-oslquery"
210+
# OSL_TEST_CPP_BACKEND=1 also exercises the C++ source-gen backend
211+
# (debug_output_cpp=3) on this variant, validating the Linux .so path.
212+
setenvs: export CTEST_EXCLUSIONS="broken|python-oslquery" OSL_TEST_CPP_BACKEND=1
211213
# ^^ exclude python-oslquery test until the ASWF container properly
212214
# includes OIIO's python bindings, then we can remove that.
213215
# Address and leak sanitizers (debug build)
@@ -484,6 +486,9 @@ jobs:
484486
cxx_std: 17
485487
python_ver: "3.14"
486488
openimageio_ver: main
489+
# OSL_TEST_CPP_BACKEND=1 also exercises the C++ source-gen backend
490+
# (debug_output_cpp=3) on macOS, validating the .dylib DSO path.
491+
setenvs: export OSL_TEST_CPP_BACKEND=1
487492

488493

489494
windows:

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ build
1919
.opencode/
2020
.github/copilot-instructions.md
2121
/specs
22+
/.cache

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ endif ()
118118
set (OSL_LIBNAME_SUFFIX "" CACHE STRING
119119
"Optional name appended to ${PROJECT_NAME} libraries that are built")
120120
option (OSL_BUILD_TESTS "Build the unit tests, testshade, testrender" ON)
121+
set_option (OSL_TEST_CPP_BACKEND "Also run testsuite cases through the C++ source-gen backend (debug_output_cpp=3)" OFF)
121122
if (WIN32)
122123
option (USE_LLVM_BITCODE "Generate embedded LLVM bitcode" OFF)
123124
else ()
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
# Specification Quality Checklist: BackendCpp — C++ Source Code Generation Backend
2+
3+
**Purpose**: Validate specification completeness and quality before proceeding to planning
4+
**Created**: 2026-05-26
5+
**Feature**: [spec.md](../spec.md)
6+
7+
## Content Quality
8+
9+
- [x] No implementation details (languages, frameworks, APIs)
10+
- [x] Focused on user value and business needs
11+
- [x] Written for non-technical stakeholders
12+
- [x] All mandatory sections completed
13+
14+
## Requirement Completeness
15+
16+
- [x] No [NEEDS CLARIFICATION] markers remain
17+
- [x] Requirements are testable and unambiguous
18+
- [x] Success criteria are measurable
19+
- [x] Success criteria are technology-agnostic (no implementation details)
20+
- [x] All acceptance scenarios are defined
21+
- [x] Edge cases are identified
22+
- [x] Scope is clearly bounded
23+
- [x] Dependencies and assumptions identified
24+
25+
## Feature Readiness
26+
27+
- [x] All functional requirements have clear acceptance criteria
28+
- [x] User scenarios cover primary flows
29+
- [x] Feature meets measurable outcomes defined in Success Criteria
30+
- [x] No implementation details leak into specification
31+
32+
## Notes
33+
34+
- SC-002 names Linux/macOS compilers — acceptable since this is a C++ project with known platform targets, not a technology choice.
35+
- Batched/SIMD and PTX/OptiX are explicitly out of scope in Assumptions.
36+
- "Compile to DSO" step is external to OSL (developer-invoked compiler); FR-008 documents rather than automates it.
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
# Data Model: BackendCpp
2+
3+
**Branch**: `002-backend-cpp` | **Date**: 2026-05-26
4+
5+
## Key Entities
6+
7+
### BackendCpp (existing, extended)
8+
9+
`OSOProcessorBase` subclass. Lives in `src/liboslexec/backendcpp.{h,cpp}`.
10+
11+
**New members added in this feature**:
12+
13+
| Member | Type | Purpose |
14+
|--------|------|---------|
15+
| `m_out` | `std::ostringstream` | Accumulates generated C++ source (already exists) |
16+
| `m_indentlevel` / `m_indentview` | `int` / `string_view` | Indentation tracking (already exists) |
17+
| *(no new members needed — generation state is transient)* | | |
18+
19+
**New methods**:
20+
21+
| Method | Virtual? | Phase | Purpose |
22+
|--------|----------|-------|---------|
23+
| `generate_groupdata_struct()` | no | 3 | Emit typed `struct GroupData { ... };` |
24+
| `generate_layer_func(int layer)` | no | 4 | Emit one layer function |
25+
| `generate_group_entry()` | no | 4 | Emit `RunLLVMGroupFunc`-compatible entry |
26+
| `compile_to_dso()` | no | 5 | Shell out to compiler via `popen`, capture errors |
27+
| `load_dso()` | no | 6 | `OIIO::Plugin::open()`, ABI check, store handle |
28+
29+
**Virtual interface** — language-specific seam points that subclasses override:
30+
31+
| Method | Default (C++) | Purpose |
32+
|--------|--------------|---------|
33+
| `lang_type_name(TypeDesc)` | `"float"`, `"int"`, `"ustringhash"`, … | Scalar/aggregate type name in target language |
34+
| `lang_sym_type_name(Symbol&)` | `"Dual2<float>"`, `"Vec3"`, … | Full symbol type including derivative wrapper |
35+
| `lang_preamble()` | `#include "osl_cpp_runtime.h"` | File header and include directives |
36+
| `lang_function_qualifier()` | `""` | Per-function qualifier (empty for C++) |
37+
| `lang_linkage_prefix()` | `extern "C"` | Linkage specifier for exported symbols |
38+
| `lang_file_extension()` | `".cpp"` | Output file extension |
39+
| `lang_ptr_syntax()` | `"*"` | Pointer declarator token |
40+
41+
The traversal logic (`run()`, `build_cpp_code()`, `generate_groupdata_struct()`, op dispatching) is non-virtual — shared across all language backends. Only the leaf emission points are virtual.
42+
43+
`cpp_typedesc_name()` and `cpp_sym_type_name()` (already in `backendcpp.h`) are renamed to `lang_type_name()` / `lang_sym_type_name()` and made virtual in Phase 4. The `cpp_` prefix would be misleading in subclasses.
44+
45+
---
46+
47+
### ShadingSystemImpl (existing, extended)
48+
49+
**New/changed members** (`src/liboslexec/oslexec_pvt.h`):
50+
51+
| Member | Old Type | New Type | Default | Purpose |
52+
|--------|----------|----------|---------|---------|
53+
| `m_debug_output_cpp` | `bool` | `int` | `0` | Escalating level: 1/2/3 |
54+
| `m_cpp_output_dir` || `std::string` | `"."` | Where `.cpp` and DSO files are written |
55+
| `m_cpp_compiler` || `std::string` | *(CMake-baked)* | Compiler executable path |
56+
| `m_cpp_compiler_flags` || `std::string` | *(CMake-baked)* | Compilation flags |
57+
58+
**New accessor**:
59+
```cpp
60+
int debug_output_cpp() const { return m_debug_output_cpp; } // was bool
61+
std::string_view cpp_output_dir() const { return m_cpp_output_dir; }
62+
std::string_view cpp_compiler() const { return m_cpp_compiler; }
63+
std::string_view cpp_compiler_flags() const { return m_cpp_compiler_flags; }
64+
```
65+
66+
**Attribute names** (ShadingSystem::attribute):
67+
- `"debug_output_cpp"` → `m_debug_output_cpp` (int)
68+
- `"cpp_output_dir"` → `m_cpp_output_dir` (string)
69+
- `"cpp_compiler"` → `m_cpp_compiler` (string)
70+
- `"cpp_compiler_flags"` → `m_cpp_compiler_flags` (string)
71+
72+
**Env var**: `OSL_DEBUG_OUTPUT_CPP` → `atoi()` → `m_debug_output_cpp` (read in ShadingSystemImpl constructor, same pattern as `OSL_LLVM_DEBUG` at `shadingsys.cpp:1243`)
73+
74+
---
75+
76+
### ShaderGroup (existing, extended)
77+
78+
**New members** (`src/liboslexec/oslexec_pvt.h`):
79+
80+
| Member | Type | Purpose |
81+
|--------|------|---------|
82+
| `m_cpp_dso_handle` | `OIIO::Plugin::Handle` | DSO handle from `OIIO::Plugin::open()`; `nullptr` when not loaded |
83+
| `m_cpp_compiled_version` | `RunLLVMGroupFunc` | Entry point resolved from DSO |
84+
85+
**Lifecycle**: `m_cpp_dso_handle` initialized to `nullptr`. Set by `BackendCpp::load_dso()` via `OIIO::Plugin::open()`. Closed via `OIIO::Plugin::close(m_cpp_dso_handle)` in `ShaderGroup` destructor if non-null.
86+
87+
---
88+
89+
### OpDescriptor (existing, unchanged)
90+
91+
```cpp
92+
struct OpDescriptor {
93+
// ...existing fields...
94+
OpCppGen cppgen { nullptr }; // already exists — null → NO CPP GENERATOR stub
95+
};
96+
```
97+
98+
`OpCppGen` = `bool (*)(BackendCpp&, int opnum)` — already defined in `oslexec_pvt.h:129`.
99+
100+
---
101+
102+
### Generated File Structure
103+
104+
One `.cpp` file per shader group, written to `cpp_output_dir/group-cpp-<name>.cpp`.
105+
106+
```
107+
#include "osl_cpp_runtime.h"
108+
109+
// --- GroupData ---
110+
struct GroupData {
111+
bool layer_runflags[N]; // field 0: rounded up to 32-bit boundary
112+
// ... userdata flags and values if any ...
113+
// ... per-layer connected/output param fields ...
114+
};
115+
116+
// --- ABI version export ---
117+
extern "C" int osl_cpp_abi_version() { return OSL_CPP_ABI_VERSION; }
118+
119+
// --- Layer functions ---
120+
static void layer_0_name(ShaderGlobals* sg, GroupData* gd,
121+
void* userdata_base, void* output_base,
122+
int shadeindex, void* interactive_params) {
123+
// local decls
124+
// op statements
125+
}
126+
// ... one per active layer ...
127+
128+
// --- Group entry function (matches RunLLVMGroupFunc) ---
129+
// As shipped, the exported entry symbol is osl_init_group_<name> (resolved by
130+
// BackendCpp::load_dso); the generated DSO exports exactly this one entry.
131+
extern "C" void osl_init_group_<name>(void* sg_, void* gd_,
132+
void* userdata_base, void* output_base,
133+
int shadeindex, void* interactive_params) {
134+
ShaderGlobals* sg = (ShaderGlobals*)sg_;
135+
GroupData* gd = (GroupData*)gd_;
136+
if (!gd->layer_runflags[N-1])
137+
layer_N_name(sg, gd, userdata_base, output_base,
138+
shadeindex, interactive_params);
139+
}
140+
```
141+
142+
---
143+
144+
### `osl_cpp_runtime.h` (new)
145+
146+
Internal header included by every generated `.cpp` file. Not installed or part of the public API.
147+
148+
**Contents**:
149+
- `#pragma once`
150+
- Forward-include of OSL types needed by generated code (`ShaderGlobals`, `Dual2<T>`, etc.)
151+
- `constexpr int OSL_CPP_ABI_VERSION = 10000 * OSL_VERSION_MAJOR + 100 * OSL_VERSION_MINOR + revision;` (as shipped; `revision = 1`) — folds in the OSL major/minor version so minor releases are link-incompatible automatically. Defined identically in `oslexec_pvt.h`; a mismatch fails the load-time ABI check loudly. (See tasks.md Phase 13 / T054.)
152+
- `extern "C"` declarations for all `osl_*` functions referenced by generated code
153+
154+
---
155+
156+
### Generated File Naming
157+
158+
| Artifact | Name pattern | Location |
159+
|----------|-------------|----------|
160+
| C++ source | `group-cpp-<name>.cpp` | `cpp_output_dir/` |
161+
| Shared library (Linux) | `group-cpp-<name>.so` | `cpp_output_dir/` |
162+
| Shared library (macOS) | `group-cpp-<name>.dylib` | `cpp_output_dir/` |
163+
| Shared library (Windows) | `group-cpp-<name>.dll` | `cpp_output_dir/` |
164+
165+
`<name>` = `group.name()` passed through `cpp_safe_name()` to ensure valid filesystem characters.
166+
167+
---
168+
169+
## State Transitions
170+
171+
```
172+
debug_output_cpp value → pipeline stages executed
173+
──────────────────────────────────────────────────
174+
0 → nothing (existing JIT path unchanged)
175+
1 → generate .cpp → write to cpp_output_dir
176+
2 → generate .cpp → write → compile via popen → .so/.dylib/.dll
177+
3 → generate .cpp → write → compile → load DSO → ABI check →
178+
store entry point in ShaderGroup → skip JIT → execute via DSO
179+
```
180+
181+
Error handling at each transition (no automatic fallback to JIT):
182+
- Write failure → `ShadingSystem::errorfmt()`
183+
- Compile failure → capture compiler stderr via `popen``errorfmt()`, group failed
184+
- Load failure → `errorfmt()`, group failed
185+
- ABI mismatch → `errorfmt()`, group failed

0 commit comments

Comments
 (0)