Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 6 additions & 5 deletions bindings/layout.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,14 @@ type Requirements struct {
// The GLES (GLSL ES 3.00), WGSL and Metal artifacts have all of this in
// core and need nothing.
GLExtensions []string `json:"glExtensions,omitempty"`
// SceneColorMips is true when the material samples the backdrop at an
// explicit mip level (sceneColorLevel). The host must render the post source
// into a texture WITH a mip chain, regenerate the mips each frame before the
// pass, and bind a sampler whose minification filter walks mips
// SceneColorMips is true when the material samples the backdrop at an explicit
// non-zero or dynamic mip level (sceneColorLevel). The host must render the
// post source into a texture WITH a mip chain, regenerate the mips each frame
// before the pass, and bind a sampler whose minification filter walks mips
// (LINEAR_MIPMAP_LINEAR / GPUFilterMode "linear" with mipmapFilter
// "linear"). Without a mip chain every backend clamps to level 0 and the
// pass renders unblurred.
// pass renders unblurred. Literal level-zero taps still use explicit LOD, but
// do not require the host to create mips.
SceneColorMips bool `json:"sceneColorMips,omitempty"`
// GLSceneSizeUniform is non-empty when the GLSL (WebGL 1 / GLSL ES 1.00)
// artifact declares a host-set backdrop-size uniform, because GLSL ES 1.00
Expand Down
4 changes: 3 additions & 1 deletion ir/emit_error.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,6 @@ func emitPanic(code, format string, args ...any) {
// constructed explicitly, as the test does.
type UnrenderableStmtCFForTests struct{}

func (UnrenderableStmtCFForTests) isStmtCF() {}
func (UnrenderableStmtCFForTests) isStmtCF() {}
func (UnrenderableStmtCFForTests) exprs() []Expr { return nil }
func (UnrenderableStmtCFForTests) nestedStmts() [][]Stmt { return nil }
3 changes: 2 additions & 1 deletion ir/emit_error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import (
// counterpart does need to be exported).
type unrenderableExprForTest struct{}

func (unrenderableExprForTest) isExpr() {}
func (unrenderableExprForTest) isExpr() {}
func (unrenderableExprForTest) children() []Expr { return nil }

// TestPrintPanicsWithEmitErrorForUnknownExpr is the direct test for
// CodeEmitUnknownExpr: Print's default case used to silently return the
Expand Down
12 changes: 7 additions & 5 deletions ir/ir.go
Original file line number Diff line number Diff line change
Expand Up @@ -340,11 +340,13 @@ type SceneSample struct {
// target). It is the backdrop counterpart of SampleLevel: one tap of a
// pre-filtered mip replaces an N-tap blur kernel.
//
// The host must supply a scene-color target that HAS mips and a sampler with a
// mipmap min filter. Without them every backend clamps to level 0 and the pass
// renders unblurred, so bindings.Layout records the requirement in
// Layout.Requires (see bindings.Requirements.SceneColorMips) for the host to
// honour or refuse.
// A non-zero or dynamic LOD requires the host to supply a scene-color target
// that HAS mips and a sampler with a mipmap min filter. Without them every
// backend clamps to level 0 and the pass renders unblurred, so bindings.Layout
// records the requirement in Layout.Requires (see
// bindings.Requirements.SceneColorMips) for the host to honour or refuse. A
// literal level-zero tap is still useful because WGSL permits
// textureSampleLevel in non-uniform control flow.
type SceneSampleLevel struct {
Name string // "sceneColor"
UV Expr
Expand Down
23 changes: 22 additions & 1 deletion ir/uses.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,34 @@ func StageUsesSceneSize(stage Stage) bool {
}

// StageUsesSceneSampleLevel reports whether stage samples the backdrop at an
// explicit LOD (which requires a mipped scene-color target on the host).
// explicit LOD. WebGL still needs its explicit-LOD extension for level-zero
// taps, even when the host does not need to build scene-color mips.
func StageUsesSceneSampleLevel(stage Stage) bool {
return stageMatches(stage, func(e Expr) bool { _, ok := e.(SceneSampleLevel); return ok })
}

// StageRequiresSceneColorMips reports whether a stage samples the backdrop at
// an explicit, non-zero or dynamic LOD. A literal level-zero tap emits
// textureSampleLevel, which is valid in non-uniform WGSL control flow, but does
// not need a host-side mip chain.
func StageRequiresSceneColorMips(stage Stage) bool {
return stageMatches(stage, func(e Expr) bool {
lvl, ok := e.(SceneSampleLevel)
return ok && !isLiteralZero(lvl.LOD)
})
}

// UsesSceneSize reports whether m's fragment stage reads the backdrop resolution.
func UsesSceneSize(m Module) bool { return StageUsesSceneSize(m.Fragment) }

// UsesSceneSampleLevel reports whether m's fragment stage samples the backdrop
// at an explicit LOD.
func UsesSceneSampleLevel(m Module) bool { return StageUsesSceneSampleLevel(m.Fragment) }

// RequiresSceneColorMips reports whether m's fragment stage needs the host to
// provide a scene-color mip chain.
func RequiresSceneColorMips(m Module) bool { return StageRequiresSceneColorMips(m.Fragment) }

// StageUsesVertexIndexBuiltin reports whether stage's body or output
// expression references the vertexIndex builtin, including a reference nested
// inside an if/for/assign/return — anywhere stmtMatches recurses.
Expand Down Expand Up @@ -100,6 +116,11 @@ func exprCalls(e Expr, names map[string]bool) bool {
})
}

func isLiteralZero(e Expr) bool {
lit, ok := e.(Lit)
return ok && lit.Value == 0
}

func stageMatches(stage Stage, pred func(Expr) bool) bool {
for _, s := range stage.Body {
if stmtMatches(s, pred) {
Expand Down
2 changes: 1 addition & 1 deletion lower/lower.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func hostRequirements(mod ir.Module) bindings.Requirements {
}
if ir.UsesSceneSampleLevel(mod) {
req.GLExtensions = append(req.GLExtensions, "EXT_shader_texture_lod")
req.SceneColorMips = true
req.SceneColorMips = ir.RequiresSceneColorMips(mod)
}
if ir.UsesSceneSize(mod) {
req.GLSceneSizeUniform = "_sceneSize"
Expand Down
39 changes: 39 additions & 0 deletions lower/post_surface_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"strings"
"testing"

"m31labs.dev/selena/emit/wgsl"
"m31labs.dev/selena/hir"
"m31labs.dev/selena/ir"
)
Expand Down Expand Up @@ -87,6 +88,44 @@ func TestSceneColorLevelLowersToSceneSampleLevel(t *testing.T) {
}
}

func TestSceneColorLevelZeroLODDoesNotRequireSceneColorMips(t *testing.T) {
m := postMaterial(nil, nil, hir.Call{Func: "sceneColorLevel", Args: []hir.Expr{
hir.Member{E: hir.Ref{Name: "post"}, Field: "uv"},
hir.Lit{Value: 0.0},
}})
mod, layout, err := Lower(m)
if err != nil {
t.Fatal(err)
}
if layout.Requires.SceneColorMips {
t.Fatal("Requires.SceneColorMips = true; literal level-zero backdrop taps do not need host mips")
}
if len(layout.Requires.GLExtensions) != 1 || layout.Requires.GLExtensions[0] != "EXT_shader_texture_lod" {
t.Fatalf("Requires.GLExtensions = %v, want EXT_shader_texture_lod", layout.Requires.GLExtensions)
}
src, err := wgsl.Emit(mod)
if err != nil {
t.Fatalf("wgsl emit: %v", err)
}
if want := "textureSampleLevel(_sceneColorTex, _sceneColorSamp, in.v_uv, 0.0)"; !strings.Contains(src, want) {
t.Fatalf("WGSL missing %q\n--- got ---\n%s", want, src)
}
}

func TestSceneColorLevelDynamicLODRequiresSceneColorMips(t *testing.T) {
m := postMaterial([]hir.Param{{Name: "lod", Type: hir.Float}}, nil, hir.Call{Func: "sceneColorLevel", Args: []hir.Expr{
hir.Member{E: hir.Ref{Name: "post"}, Field: "uv"},
hir.Ref{Name: "lod"},
}})
_, layout, err := Lower(m)
if err != nil {
t.Fatal(err)
}
if !layout.Requires.SceneColorMips {
t.Fatal("Requires.SceneColorMips = false; dynamic backdrop LOD still needs host mips")
}
}

// TestSceneColorLevelArgumentTypes keeps the signature honest.
func TestSceneColorLevelArgumentTypes(t *testing.T) {
uv := hir.Member{E: hir.Ref{Name: "post"}, Field: "uv"}
Expand Down
Loading