| name | slang-shader-authoring |
| description | Write, register, and debug Slang shaders in epix_engine. Use when authoring .slang shader files, wiring them into the ShaderCache/ShaderLoader pipeline, adding preprocessor defs variants, resolving import errors, or diagnosing Slang compile failures. Covers entry points, module declarations, VFS import rules, and C++ integration API. |
| argument-hint | [shader feature to implement or error to diagnose] |
Slang Shader Authoring
What This Produces
A working Slang shader file + the C++ wiring to load, compile, and use it at runtime through epix_shader's ShaderCache.
Slang Language Rules (Epix-Specific)
Module Declaration
// CORRECT — simple identifier
module scene;
// CORRECT — string-literal for dotted/path names
module "epix/view"; // use this for any name containing / or .
// WRONG — dots NOT allowed in bare identifier form
module epix.view; // ❌ syntax error
Visibility
All exported symbols must be marked public — Slang defaults to internal:
public struct Vertex { // ✅
public float3 position; // ✅ members too
float2 uv; // ❌ internal — invisible to importers
}
Entry Points
Use [shader("...")] attribute. Entry point names are auto-discovered:
[shader("compute")]
[numthreads(8, 8, 1)]
void computeMain(uint3 id : SV_DispatchThreadID) { }
[shader("vertex")]
VertexOutput vertexMain(VertexInput input) { }
[shader("fragment")]
float4 fragmentMain(VertexOutput input) : SV_Target { }
Epix sets VulkanUseEntryPointName = true — entry point names are preserved in SPIR-V output. Name them consistently so C++ pipeline creation can reference them by string.
Instance ID
SV_InstanceID requires DrawParameters SPIR-V capability (not supported in WebGPU).
Use SV_VulkanInstanceID instead:
// WRONG
uint instanceID : SV_InstanceID // ❌ unsupported capability
// CORRECT
uint instanceID : SV_VulkanInstanceID // ✅
Matrix Layout
Matrices are column-major (Vulkan convention). Pass data accordingly from C++.
Import Patterns
| Style | Example | Resolution |
|---|
| Custom module name | import utility; | Registered in ShaderCache as a custom import |
| Root-relative file | import "/shared/view.slang"; | Loaded as AssetPath from root of same source as this shader |
| Full-path file | import "source://shaders/lighting.slang"; | Loaded as AssetPath from root of given source |
| Relative file | import "common/math.slang"; | Relative to the importing shader's directory |
__include | __include "defs.slang"; | Textual include; included file must declare implementing <module>;, the path search follows the same rules as import |
Bare identifier import (import foo;) → Slang resolves to foo.slang via the VFS. Registration in cache as a ShaderImport::custom("foo") entry is required.
C++ Integration API
1 — Create a Shader object
#import epix.shader;
auto shader = Shader::from_slang(source_string, "my_shader.slang");
auto shader = Shader::from_slang_with_defs(source, "my_shader.slang", {
ShaderDefVal::from_int("BLOCK_SIZE", 8),
ShaderDefVal::from_bool("ENABLE_SHADOWS", true),
});
Handle<Shader> handle = asset_server.load<Shader>("shaders/my_shader.slang");
Handle<Shader> handle = asset_server.load<Shader>("source://shaders/my_shader.slang");
2 — Register in ShaderCache
assets::AssetId<Shader> id = ...;
cache.set_shader(id, std::move(shader));
3 — Compile at runtime
auto result = cache.get(pipeline_id, id, {});
auto result = cache.get(pipeline_id, id, {
ShaderDefVal::from_int("BLOCK_SIZE", 4),
});
if (result) {
wgpu::ShaderModule module = result.value();
} else {
auto& err = result.error();
if (err.is_recoverable()) {
} else {
}
}
Asset Pipeline Workflow (Files on Disk)
- Place
.slang file under assets/shaders/.
- Load it via
AssetServer::load<Shader>(AssetPath("shaders/my_shader.slang")).
- The
ShaderLoader parses import statements and automatically queues file-based dependencies as additional Handle<Shader> loads.
- Custom module imports (bare identifier style) must be pre-registered in the cache before
cache.get() is called.
- Slang source is not pre-compiled during asset preprocessing — it compiles on the first
cache.get() call. Subsequent calls with the same def set return the cached wgpu::ShaderModule.
Procedure: Authoring a New Shader
Step 1 — Write the .slang file
// assets/shaders/my_pass.slang
module "shaders/my_pass";
import "/shared/common.slang"; // file dep, loaded by ShaderLoader
public struct PushConstants {
public float4x4 transform;
}
ParameterBlock<PushConstants> pc;
[shader("vertex")]
float4 vertexMain(float3 pos : POSITION) : SV_Position {
return mul(pc.transform, float4(pos, 1.0));
}
[shader("fragment")]
float4 fragmentMain() : SV_Target {
return float4(1, 0, 0, 1);
}
Step 2 — Load and register
void setup_shaders(Res<AssetServer> server, ResMut<ShaderCache> cache) {
auto handle = server->load<Shader>(AssetPath("shaders/my_pass.slang"));
}
Step 3 — Compile and use
void render(ResMut<ShaderCache> cache, Res<MyShaderIds> ids) {
auto result = cache.get(pipeline_id, ids->my_pass, {});
if (!result) { return; }
auto& module = result.value();
}
Plugin-Managed Automation
When using epix_shader and epix_render plugins together, most of the manual wiring described above is handled automatically. Do not duplicate it.
ShaderPlugin (epix::shader::ShaderPlugin)
Added automatically when the render plugin is used. It:
- Registers
Shader as an asset type with ShaderLoader (handles .wgsl and .slang files) and ShaderProcessor.
- Registers a
sync shader cache system in the core::Last schedule that calls cache->sync(events, shaders) after every AssetEvent<Shader>. This populates ShaderCache automatically when shaders finish loading — you do not call cache.set_shader() manually.
PipelineServer (epix::render::PipelineServer)
Inserted as a resource into the render world. It:
- Runs
extract_shaders every ExtractSchedule tick — extracts Assets<Shader> and shader events from the main world, syncs them, and re-queues any pipeline whose shader changed.
- Runs
process_pipeline_system every RenderSet::Render tick — advances pending pipelines from Queued through async thread-pool compilation to Pipeline or Error state.
- Automatically re-queues affected pipelines when any of their shaders are updated via
AssetEvent.
What you still do manually
Handle<Shader> h = server->load<Shader>(AssetPath("shaders/my_pass.slang"));
CachedPipelineId id = pipeline_server->queue_render_pipeline(descriptor);
auto result = pipeline_server->get_render_pipeline(id);
if (!result) { return; }
auto& pipeline = result.value().get();
States a pipeline transitions through: PipelineStateQueued → PipelineStateCreating (async future) → Pipeline (ready) or PipelineServerError.
Custom module imports (bare identifier import foo;) still need to be registered before the first compile succeeds. An unregistered custom import produces a recoverable ShaderCacheError and the pipeline stays in Queued state until the import appears.
Diagnosing Compile Errors
| Error stage | Meaning | Fix |
|---|
SessionCreation | Bad options/target | Check SPIR-V target setup |
ModuleLoad | Syntax / parse error | Check Slang syntax; look at err.message() for line |
Compose | Entry point not found | Confirm [shader("...")] on the function |
Link | Unresolved import | Register missing module in cache; check import path |
CodeGeneration | SPIR-V capability issue | Replace SV_InstanceID → SV_VulkanInstanceID; check unsupported intrinsics |
Recoverable errors (return is_recoverable() == true): a custom-import dependency hasn't been registered yet. Safe to retry next frame.
Checklist Before Submitting a Shader