diff --git a/builtin-programs/gpu/draw.folk b/builtin-programs/gpu/draw.folk index c59db073..601f8b6f 100644 --- a/builtin-programs/gpu/draw.folk +++ b/builtin-programs/gpu/draw.folk @@ -13,7 +13,8 @@ if {[info exists this] && $::tcl_platform(os) eq "darwin"} { When the GPU library is /gpuLib/ &\ the GPU Vulkan handle type definer is /defineVulkanHandleType/ &\ the GPU texture library is /gpuTextureLib/ &\ - the GPU pipeline library is /pipelineLib/ { + the GPU pipeline library is /pipelineLib/ &\ + the GPU pipeline compiler library is /pipelineCompilerLib/ { fn defineVulkanHandleType @@ -525,359 +526,6 @@ proc makeGpu {gpuLib pipelineLib drawLib} { return [library create gpu {gpuLib p tailcall "$drawLib $drawLibCmd" {*}$args } } - - # Construct a reusable GLSL function that can be linked into and - # called from a shader/pipeline. - proc fn {fnDict arguments rtype body} { - set fnArgs [list] - # We inline all dependent functions from the caller scope - # immediately here, since we don't know if those dependencies - # would be accessible/in scope at all when this function gets - # actually compiled into a shader. - set depFnDict [dict create] - foreach {argtype argname} $arguments { - if {$argtype eq "fn"} { - # TODO: Support fn being a list {fnName fn}. - if {![dict exists $fnDict $argname]} { - puts stderr "Gpu::fn: $argname not found" - return -code 99 $argname - } - dict set depFnDict [string map {: ""} $argname] \ - [dict get $fnDict $argname] - } else { - lappend fnArgs $argtype $argname - } - } - return [list $fnArgs $depFnDict $rtype $body] - } - - # Construct a shader pipeline that can be used to draw to the - # screen. - proc pipeline {fnDict args} { - variable gpuLib - variable pipelineLib - variable drawLib - - if {[llength $args] == 3} { - lassign $args vertArgs vertBody fragBody - set fragArgs [list] - } elseif {[llength $args] == 4} { - lassign $args vertArgs vertBody fragArgs fragBody - } else { - error {Gpu pipeline: should be used as [$gpu pipeline vertArgs vertBody fragBody], or [$gpu pipeline vertArgs vertBody fragArgs fragBody]} - } - set vertFnDict [dict create] - set fragFnDict [dict create] - set pushConstants [list] - foreach {argtype argname} $vertArgs { - if {$argtype eq "fn"} { - # TODO: Support fn being a list {name fn}. - if {![dict exists $fnDict $argname]} { - return -code 99 $argname - } - set fn [dict get $fnDict $argname] - set vertFnDict [dict merge $vertFnDict [lindex $fn 1]] - dict set vertFnDict $argname $fn - continue - } - lappend pushConstants $argtype $argname - } - foreach {argtype argname} $fragArgs { - if {$argtype eq "fn"} { - # TODO: Support fn being a list {name fn}. - if {![dict exists $fnDict $argname]} { - return -code 99 $argname - } - set fn [dict get $fnDict $argname] - set fragFnDict [dict merge $fragFnDict [lindex $fn 1]] - dict set fragFnDict $argname $fn - continue - } else { - error "Fragment arguments not supported" - } - } - - # Create a C subcompiler to create a fast routine to encode - # the push constants on each draw call. - set cc [C] - $cc typedef int sampler2D - $cc struct vec2 { float x; float y; } - $cc struct vec3 { float x; float y; float z; } - $cc struct vec4 { float x; float y; float z; float w; } - $cc struct uvec4 { uint32_t x; uint32_t y; uint32_t z; uint32_t w; } - # Note that mat3 is COLUMN-MAJOR and every column has 1 float - # of padding at the end. - $cc struct mat3 { float data[12]; } - - $cc argtype vec2 { - vec2 $argname; - { - int $[set argname]_objc = Jim_ListLength(interp, $obj); - __ENSURE($[set argname]_objc == 2); - double x; double y; - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); - $argname = (vec2) { (float)x, (float)y }; - } - } - $cc argtype vec3 { - vec3 $argname; - { - int $[set argname]_objc = Jim_ListLength(interp, $obj); - __ENSURE($[set argname]_objc == 3); - double x; double y; double z; - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); - $argname = (vec3) { (float)x, (float)y, (float)z }; - } - } - $cc argtype vec4 { - vec4 $argname; - { - int $[set argname]_objc = Jim_ListLength(interp, $obj); - __ENSURE($[set argname]_objc == 4); - double x; double y; double z; double w; - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 3), &w)); - $argname = (vec4) { (float)x, (float)y, (float)z, (float)w }; - } - } - $cc argtype uvec4 { - uvec4 $argname; - { - int $[set argname]_objc = Jim_ListLength(interp, $obj); - __ENSURE($[set argname]_objc == 4); - jim_wide x; jim_wide y; jim_wide z; jim_wide w; - __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); - __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); - __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); - __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 3), &w)); - $argname = (uvec4) { (uint32_t)x, (uint32_t)y, (uint32_t)z, (uint32_t)w }; - } - } - # Note that we take matrices from Tcl in ROW-MAJOR form and - # convert them to column-major form inline here. - $cc argtype mat3 { - mat3 $argname; - { - int $[set argname]_objc = Jim_ListLength(interp, $obj); - __ENSURE($[set argname]_objc == 3); - for (int y = 0; y < 3; y++) { - Jim_Obj* rowObj = Jim_ListGetIndex(interp, $obj, y); - __ENSURE(Jim_ListLength(interp, rowObj) == 3); - for (int x = 0; x < 3; x++) { - double el; - __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, rowObj, x), &el)); - - int i = x * 4 + y; - $argname.data[i] = el; - } - } - } - } - $cc code [csubst { - typedef struct Args { - $[join [lmap {argtype argname} $pushConstants { - set alignas [expr {$argtype eq "mat3" ? "16" : "sizeof($argtype)"}] - subst {_Alignas($alignas) $argtype $argname;} - }] "\n"] - } Args; - - typedef struct PushConstantsEncoder { - int (*encode)(Jim_Interp* interp, Jim_Obj* obj, uint8_t out[128]); - } PushConstantsEncoder; - - static uint8_t argsBuf[128]; - }] - $cc include - $cc proc getArgsSize {} int { return sizeof(Args); } - $cc proc encodeArgs $pushConstants void { - Args args = {$[join [lmap {argtype argname} $pushConstants { subst {.$argname = $argname} }] " ,"]}; - memcpy(argsBuf, &args, sizeof(args)); - } - # This is what gets saved as the PushConstantsEncoder and - # called at draw time. - $cc proc encodeObj {Jim_Interp* interp Jim_Obj* obj uint8_t* out} int { - int objc = Jim_ListLength(interp, obj); - Jim_Obj* objv[1 + objc]; - for (int i = 0; i < objc; i++) { - objv[1 + i] = Jim_ListGetIndex(interp, obj, i); - } - - int ret = encodeArgs_Cmd(interp, 1 + objc, objv); - if (ret != JIM_OK) { - // You CANNOT use FOLK_ENSURE here, because this is - // passed as function pointer and does not capture the - // correct jmp_buf for the caller. - return -1; - } - - memcpy(out, argsBuf, sizeof(Args)); - return sizeof(Args); - } - $cc proc makeEncoder {} PushConstantsEncoder* { - PushConstantsEncoder* encoder = malloc(sizeof(PushConstantsEncoder)); - encoder->encode = encodeObj; - return encoder; - } - set encoderLib [$cc compile] - - set encodePushConstants [$encoderLib makeEncoder] - set pushConstantsSize [$encoderLib getArgsSize] - - set pushConstantsCode [if {[llength $pushConstants] > 0} { - subst { - layout(push_constant) uniform Args { - [join [lmap {argtype argname} $pushConstants { - if {$argname eq "_"} continue - if {$argtype eq "sampler2D"} { - expr {"int $argname;"} - } else { - expr {"$argtype $argname;"} - } - }] "\n"] - } args; - } - }] - - set vertShaderModule [$pipelineLib createShaderModule [glslc -fshader-stage=vert [csubst { - #version 450 - - $pushConstantsCode - - $[join [lmap {fnName fn} $vertFnDict { - lassign $fn fnArgs _ fnRtype fnBody - subst { - $fnRtype $fnName ([join [lmap {fnArgtype fnArgname} $fnArgs {subst {$fnArgtype $fnArgname}}] ", "]) { - $fnBody - } - } - }] "\n"] - - vec4 vert() { - $[join [lmap {argtype argname} $pushConstants { - if {$argname eq "_"} continue - if {$argtype eq "sampler2D"} continue - expr {"$argtype $argname = args.$argname;"} - }] " "] - $vertBody - } - - void main() { - gl_Position = vert(); - } - }]]] - # We pass the descriptor set with all textures (samplers) to all - # fragment shaders, so we never need to rebind it (at draw - # time, the shader may get an index into the array if it's - # meant to draw an texture). - # - # Note that we have individual combined image+samplers, - # instead of 1 global sampler and multiple images/textures, - # because that's the only way to allow each texture to have - # its own dimensions (dimensions are a property bound to the - # sampler). - # - # We have a whole code path basically just to handle v3dv - # (Raspberry Pi GPU), which doesn't support dynamic indexing - # (based on push constant) into the descriptor array. On GPUs - # like that, we manually emit an if ladder that checks each - # possible value of the push constant and uses the right - # statically-indexed descriptor. - set gpuSupportsDynamicIndexing [$gpuLib getDoesSupportShaderSampledImageArrayDynamicIndexing] - set fragShaderModule [$pipelineLib createShaderModule [glslc -fshader-stage=frag [csubst { - #version 450 - - layout(set = 0, binding = 0) uniform sampler2D _samplers[$[$gpuLib getMaxTextures]]; - - $pushConstantsCode - - layout(location = 0) out vec4 outColor; - - $[join [lmap {fnName fn} $fragFnDict { - lassign $fn fnArgs _ fnRtype fnBody - subst { - $fnRtype $fnName ([join [lmap {fnArgtype fnArgname} $fnArgs {subst {$fnArgtype $fnArgname}}] ", "]) { - $fnBody - } - } - }] "\n"] - - vec4 frag($[join [lmap {argtype argname} $pushConstants { - if {$argname eq "_"} continue - expr {"$argtype $argname"} - }] ", "]) { - $fragBody - } - - $[eval { - set samplerIdxs [lsearch -all -exact $pushConstants sampler2D] - proc emitFragInvocation {gpuSupportsDynamicIndexing pushConstants} { subst { - vec4 rawColor = frag([join [lmap {argtype argname} $pushConstants { - if {$argname eq "_"} continue - if {$argtype eq "sampler2D"} { - if {$gpuSupportsDynamicIndexing} { - expr {"_samplers\[args.$argname\]"} - } else { - # This should have been patched to a - # static expression like - # `_samplers[3]` by the caller. - expr {"$argname"} - } - } else { - expr {"args.$argname"} - } - }] ", "]); - - // Premultiply the RGB - outColor = vec4(rawColor.rgb * rawColor.a, rawColor.a); - }} - list - }] - void main() { - $[if {$gpuSupportsDynamicIndexing || [llength $samplerIdxs] == 0} { - emitFragInvocation $gpuSupportsDynamicIndexing $pushConstants - - } elseif {[llength $samplerIdxs] == 1} { - set samplerIdx [+ [lindex $samplerIdxs 0] 1] - set samplerName [lindex $pushConstants $samplerIdx] - set maxTextures [$gpuLib getMaxTextures] - set xs [list] - for {set i 0} {$i < $maxTextures} {incr i} { - set patchedPushConstants [lreplace $pushConstants $samplerIdx $samplerIdx \ - _samplers\[$i\]] - lappend xs [subst { - [expr {$i == 0 ? "if" : "else if"}] (args.$samplerName == $i) { - [emitFragInvocation $gpuSupportsDynamicIndexing $patchedPushConstants] - } - }] - } - join $xs "\n" - } else { - error "display: Cannot currently compile a shader that has more than 1 sampler2D parameter on this GPU." - }] - } - }]]] - - # pipeline needs to contain a specification of push constants, - # so they can be filled in at draw time. - set pipeline [$pipelineLib createPipeline \ - $vertShaderModule $fragShaderModule \ - $encodePushConstants \ - $pushConstantsSize] - return $pipeline - } - - proc glslc {args} { - set cmdargs [lreplace $args end end] - set glsl [lindex $args end] - set glslfile [file tempfile /tmp/glslfileXXXXXX].glsl - set glslfd [open $glslfile w]; puts $glslfd $glsl; close $glslfd - split [string map {\n ""} [exec glslc {*}$cmdargs -mfmt=num -o - $glslfile]] "," - } }] } set drawLib [$cc compile] @@ -906,7 +554,7 @@ fn QueryAllFns! {} { fn tryCompileFn {wisher name source} { try { set fns [QueryAllFns!] - set fn [$gpu fn $fns {*}$source] + set fn [$pipelineCompilerLib fn $fns {*}$source] # Technically a misnomer: the function is just stored, not # compiled until it's been inlined into a shader. @@ -926,7 +574,7 @@ fn tryCompileFn {wisher name source} { fn tryCompilePipeline {wisher name source} { try { set fns [QueryAllFns!] - set pipeline [$gpu pipeline $fns {*}$source] + set pipeline [$pipelineCompilerLib pipeline $fns {*}$source] puts "gpu: tryCompilePipeline: Compiled $name" Claim the GPU compiles pipeline $name to $pipeline diff --git a/builtin-programs/gpu/pipelines.folk b/builtin-programs/gpu/pipelines.folk index 2c248493..1313237a 100644 --- a/builtin-programs/gpu/pipelines.folk +++ b/builtin-programs/gpu/pipelines.folk @@ -295,4 +295,365 @@ set pipelineLib [$cc compile] $pipelineLib pipelinesInit Claim the GPU pipeline library is $pipelineLib +# Pipeline compiler: Tcl-level library that takes GLSL fragments, +# routes them through glslc, builds a C subcompiler for push-constant +# encoding, and produces a Pipeline via createPipeline above. +set pipelineCompilerLib [library create pipelineCompiler {gpuLib pipelineLib} { + variable gpuLib + variable pipelineLib + + # Construct a reusable GLSL function that can be linked into and + # called from a shader/pipeline. + proc fn {fnDict arguments rtype body} { + set fnArgs [list] + # We inline all dependent functions from the caller scope + # immediately here, since we don't know if those dependencies + # would be accessible/in scope at all when this function gets + # actually compiled into a shader. + set depFnDict [dict create] + foreach {argtype argname} $arguments { + if {$argtype eq "fn"} { + # TODO: Support fn being a list {fnName fn}. + if {![dict exists $fnDict $argname]} { + puts stderr "pipelineCompiler::fn: $argname not found" + return -code 99 $argname + } + dict set depFnDict [string map {: ""} $argname] \ + [dict get $fnDict $argname] + } else { + lappend fnArgs $argtype $argname + } + } + return [list $fnArgs $depFnDict $rtype $body] + } + + # Construct a shader pipeline that can be used to draw to the + # screen. + proc pipeline {fnDict args} { + variable gpuLib + variable pipelineLib + + if {[llength $args] == 3} { + lassign $args vertArgs vertBody fragBody + set fragArgs [list] + } elseif {[llength $args] == 4} { + lassign $args vertArgs vertBody fragArgs fragBody + } else { + error {pipelineCompiler pipeline: should be used as [$pipelineCompiler pipeline vertArgs vertBody fragBody], or [$pipelineCompiler pipeline vertArgs vertBody fragArgs fragBody]} + } + set vertFnDict [dict create] + set fragFnDict [dict create] + set pushConstants [list] + foreach {argtype argname} $vertArgs { + if {$argtype eq "fn"} { + # TODO: Support fn being a list {name fn}. + if {![dict exists $fnDict $argname]} { + return -code 99 $argname + } + set fn [dict get $fnDict $argname] + set vertFnDict [dict merge $vertFnDict [lindex $fn 1]] + dict set vertFnDict $argname $fn + continue + } + lappend pushConstants $argtype $argname + } + foreach {argtype argname} $fragArgs { + if {$argtype eq "fn"} { + # TODO: Support fn being a list {name fn}. + if {![dict exists $fnDict $argname]} { + return -code 99 $argname + } + set fn [dict get $fnDict $argname] + set fragFnDict [dict merge $fragFnDict [lindex $fn 1]] + dict set fragFnDict $argname $fn + continue + } else { + error "Fragment arguments not supported" + } + } + + # Create a C subcompiler to create a fast routine to encode + # the push constants on each draw call. + set cc [C] + $cc typedef int sampler2D + $cc struct vec2 { float x; float y; } + $cc struct vec3 { float x; float y; float z; } + $cc struct vec4 { float x; float y; float z; float w; } + $cc struct uvec4 { uint32_t x; uint32_t y; uint32_t z; uint32_t w; } + # Note that mat3 is COLUMN-MAJOR and every column has 1 float + # of padding at the end. + $cc struct mat3 { float data[12]; } + + $cc argtype vec2 { + vec2 $argname; + { + int $[set argname]_objc = Jim_ListLength(interp, $obj); + __ENSURE($[set argname]_objc == 2); + double x; double y; + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); + $argname = (vec2) { (float)x, (float)y }; + } + } + $cc argtype vec3 { + vec3 $argname; + { + int $[set argname]_objc = Jim_ListLength(interp, $obj); + __ENSURE($[set argname]_objc == 3); + double x; double y; double z; + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); + $argname = (vec3) { (float)x, (float)y, (float)z }; + } + } + $cc argtype vec4 { + vec4 $argname; + { + int $[set argname]_objc = Jim_ListLength(interp, $obj); + __ENSURE($[set argname]_objc == 4); + double x; double y; double z; double w; + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, $obj, 3), &w)); + $argname = (vec4) { (float)x, (float)y, (float)z, (float)w }; + } + } + $cc argtype uvec4 { + uvec4 $argname; + { + int $[set argname]_objc = Jim_ListLength(interp, $obj); + __ENSURE($[set argname]_objc == 4); + jim_wide x; jim_wide y; jim_wide z; jim_wide w; + __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 0), &x)); + __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 1), &y)); + __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 2), &z)); + __ENSURE_OK(Jim_GetWide(interp, Jim_ListGetIndex(interp, $obj, 3), &w)); + $argname = (uvec4) { (uint32_t)x, (uint32_t)y, (uint32_t)z, (uint32_t)w }; + } + } + # Note that we take matrices from Tcl in ROW-MAJOR form and + # convert them to column-major form inline here. + $cc argtype mat3 { + mat3 $argname; + { + int $[set argname]_objc = Jim_ListLength(interp, $obj); + __ENSURE($[set argname]_objc == 3); + for (int y = 0; y < 3; y++) { + Jim_Obj* rowObj = Jim_ListGetIndex(interp, $obj, y); + __ENSURE(Jim_ListLength(interp, rowObj) == 3); + for (int x = 0; x < 3; x++) { + double el; + __ENSURE_OK(Jim_GetDouble(interp, Jim_ListGetIndex(interp, rowObj, x), &el)); + + int i = x * 4 + y; + $argname.data[i] = el; + } + } + } + } + $cc code [csubst { + typedef struct Args { + $[join [lmap {argtype argname} $pushConstants { + set alignas [expr {$argtype eq "mat3" ? "16" : "sizeof($argtype)"}] + subst {_Alignas($alignas) $argtype $argname;} + }] "\n"] + } Args; + + typedef struct PushConstantsEncoder { + int (*encode)(Jim_Interp* interp, Jim_Obj* obj, uint8_t out[128]); + } PushConstantsEncoder; + + static uint8_t argsBuf[128]; + }] + $cc include + $cc proc getArgsSize {} int { return sizeof(Args); } + $cc proc encodeArgs $pushConstants void { + Args args = {$[join [lmap {argtype argname} $pushConstants { subst {.$argname = $argname} }] " ,"]}; + memcpy(argsBuf, &args, sizeof(args)); + } + # This is what gets saved as the PushConstantsEncoder and + # called at draw time. + $cc proc encodeObj {Jim_Interp* interp Jim_Obj* obj uint8_t* out} int { + int objc = Jim_ListLength(interp, obj); + Jim_Obj* objv[1 + objc]; + for (int i = 0; i < objc; i++) { + objv[1 + i] = Jim_ListGetIndex(interp, obj, i); + } + + int ret = encodeArgs_Cmd(interp, 1 + objc, objv); + if (ret != JIM_OK) { + // You CANNOT use FOLK_ENSURE here, because this is + // passed as function pointer and does not capture the + // correct jmp_buf for the caller. + return -1; + } + + memcpy(out, argsBuf, sizeof(Args)); + return sizeof(Args); + } + $cc proc makeEncoder {} PushConstantsEncoder* { + PushConstantsEncoder* encoder = malloc(sizeof(PushConstantsEncoder)); + encoder->encode = encodeObj; + return encoder; + } + set encoderLib [$cc compile] + + set encodePushConstants [$encoderLib makeEncoder] + set pushConstantsSize [$encoderLib getArgsSize] + + set pushConstantsCode [if {[llength $pushConstants] > 0} { + subst { + layout(push_constant) uniform Args { + [join [lmap {argtype argname} $pushConstants { + if {$argname eq "_"} continue + if {$argtype eq "sampler2D"} { + expr {"int $argname;"} + } else { + expr {"$argtype $argname;"} + } + }] "\n"] + } args; + } + }] + + set vertShaderModule [$pipelineLib createShaderModule [glslc -fshader-stage=vert [csubst { + #version 450 + + $pushConstantsCode + + $[join [lmap {fnName fn} $vertFnDict { + lassign $fn fnArgs _ fnRtype fnBody + subst { + $fnRtype $fnName ([join [lmap {fnArgtype fnArgname} $fnArgs {subst {$fnArgtype $fnArgname}}] ", "]) { + $fnBody + } + } + }] "\n"] + + vec4 vert() { + $[join [lmap {argtype argname} $pushConstants { + if {$argname eq "_"} continue + if {$argtype eq "sampler2D"} continue + expr {"$argtype $argname = args.$argname;"} + }] " "] + $vertBody + } + + void main() { + gl_Position = vert(); + } + }]]] + # We pass the descriptor set with all textures (samplers) to all + # fragment shaders, so we never need to rebind it (at draw + # time, the shader may get an index into the array if it's + # meant to draw an texture). + # + # Note that we have individual combined image+samplers, + # instead of 1 global sampler and multiple images/textures, + # because that's the only way to allow each texture to have + # its own dimensions (dimensions are a property bound to the + # sampler). + # + # We have a whole code path basically just to handle v3dv + # (Raspberry Pi GPU), which doesn't support dynamic indexing + # (based on push constant) into the descriptor array. On GPUs + # like that, we manually emit an if ladder that checks each + # possible value of the push constant and uses the right + # statically-indexed descriptor. + set gpuSupportsDynamicIndexing [$gpuLib getDoesSupportShaderSampledImageArrayDynamicIndexing] + set fragShaderModule [$pipelineLib createShaderModule [glslc -fshader-stage=frag [csubst { + #version 450 + + layout(set = 0, binding = 0) uniform sampler2D _samplers[$[$gpuLib getMaxTextures]]; + + $pushConstantsCode + + layout(location = 0) out vec4 outColor; + + $[join [lmap {fnName fn} $fragFnDict { + lassign $fn fnArgs _ fnRtype fnBody + subst { + $fnRtype $fnName ([join [lmap {fnArgtype fnArgname} $fnArgs {subst {$fnArgtype $fnArgname}}] ", "]) { + $fnBody + } + } + }] "\n"] + + vec4 frag($[join [lmap {argtype argname} $pushConstants { + if {$argname eq "_"} continue + expr {"$argtype $argname"} + }] ", "]) { + $fragBody + } + + $[eval { + set samplerIdxs [lsearch -all -exact $pushConstants sampler2D] + proc emitFragInvocation {gpuSupportsDynamicIndexing pushConstants} { subst { + vec4 rawColor = frag([join [lmap {argtype argname} $pushConstants { + if {$argname eq "_"} continue + if {$argtype eq "sampler2D"} { + if {$gpuSupportsDynamicIndexing} { + expr {"_samplers\[args.$argname\]"} + } else { + # This should have been patched to a + # static expression like + # `_samplers[3]` by the caller. + expr {"$argname"} + } + } else { + expr {"args.$argname"} + } + }] ", "]); + + // Premultiply the RGB + outColor = vec4(rawColor.rgb * rawColor.a, rawColor.a); + }} + list + }] + void main() { + $[if {$gpuSupportsDynamicIndexing || [llength $samplerIdxs] == 0} { + emitFragInvocation $gpuSupportsDynamicIndexing $pushConstants + + } elseif {[llength $samplerIdxs] == 1} { + set samplerIdx [+ [lindex $samplerIdxs 0] 1] + set samplerName [lindex $pushConstants $samplerIdx] + set maxTextures [$gpuLib getMaxTextures] + set xs [list] + for {set i 0} {$i < $maxTextures} {incr i} { + set patchedPushConstants [lreplace $pushConstants $samplerIdx $samplerIdx \ + _samplers\[$i\]] + lappend xs [subst { + [expr {$i == 0 ? "if" : "else if"}] (args.$samplerName == $i) { + [emitFragInvocation $gpuSupportsDynamicIndexing $patchedPushConstants] + } + }] + } + join $xs "\n" + } else { + error "display: Cannot currently compile a shader that has more than 1 sampler2D parameter on this GPU." + }] + } + }]]] + + # pipeline needs to contain a specification of push constants, + # so they can be filled in at draw time. + set pipeline [$pipelineLib createPipeline \ + $vertShaderModule $fragShaderModule \ + $encodePushConstants \ + $pushConstantsSize] + return $pipeline + } + + proc glslc {args} { + set cmdargs [lreplace $args end end] + set glsl [lindex $args end] + set glslfile [file tempfile /tmp/glslfileXXXXXX].glsl + set glslfd [open $glslfile w]; puts $glslfd $glsl; close $glslfd + split [string map {\n ""} [exec glslc {*}$cmdargs -mfmt=num -o - $glslfile]] "," + } +}] +Claim the GPU pipeline compiler library is $pipelineCompilerLib + } diff --git a/builtin-programs/gpu/toy-shader.folk b/builtin-programs/gpu/toy-shader.folk new file mode 100644 index 00000000..57689a11 --- /dev/null +++ b/builtin-programs/gpu/toy-shader.folk @@ -0,0 +1,146 @@ +# sha1 for stable, content-addressed pipeline names. +set cc [C] +$cc include +$cc include +$cc proc sha1 {char* d} Jim_Obj* { + unsigned char md[20]; + SHA1((unsigned char *)d, strlen(d), md); + return Jim_NewStringObj(interp, (char *)md, 20); +} +$cc endcflags -lssl -lcrypto +set sha1Lib [$cc compile] + +When the GPU Vulkan handle type definer is /defineVulkanHandleType/ &\ + the GPU pipeline library is /pipelineLib/ { + +# Custom push-constants encoder for ShaderToy uniforms. +# Layout (std430): +# offset 0 vec3 iResolution (12 bytes — no trailing pad, +# since the next member is a scalar) +# offset 12 float iTime (4 bytes) +# +# Wish arguments: [list $resolution $iTime] +set cc [C] +$cc include +$cc include +$cc code { + // HACK: copied from pipelines.folk + typedef struct PushConstantsEncoder { + int (*encode)(Jim_Interp* interp, Jim_Obj* obj, uint8_t out[128]); + } PushConstantsEncoder; + + typedef struct Args { + float iResolution[3]; + float iTime; + } Args; + + static int encodeToy(Jim_Interp* interp, Jim_Obj* obj, uint8_t out[128]) { + Args args = {0}; + + // iResolution: caller passes {width height}; we synthesize z=1. + Jim_Obj* resObj = Jim_ListGetIndex(interp, obj, 0); + if (Jim_ListLength(interp, resObj) != 2) return -1; + double w, h; + if (Jim_GetDouble(interp, Jim_ListGetIndex(interp, resObj, 0), &w) != JIM_OK) return -1; + if (Jim_GetDouble(interp, Jim_ListGetIndex(interp, resObj, 1), &h) != JIM_OK) return -1; + args.iResolution[0] = (float)w; + args.iResolution[1] = (float)h; + args.iResolution[2] = 1.0f; + + // iTime: seconds since shader load. + double t; + if (Jim_GetDouble(interp, Jim_ListGetIndex(interp, obj, 1), &t) != JIM_OK) return -1; + args.iTime = (float)t; + + memcpy(out, &args, sizeof(args)); + return sizeof(args); + } +} +$cc proc makeToyEncoder {} PushConstantsEncoder* { + PushConstantsEncoder* e = malloc(sizeof(PushConstantsEncoder)); + e->encode = encodeToy; + return e; +} +$cc proc getToyArgsSize {} int { return sizeof(Args); } +set toyEncoderLib [$cc compile] + +set toyEncoder [$toyEncoderLib makeToyEncoder] +set toyArgsSize [$toyEncoderLib getToyArgsSize] + +# Run glslc as a subprocess to turn GLSL into SPIR-V words. +fn toyGlslc {stage glsl} { + set glslfile [file tempfile /tmp/toyshaderXXXXXX].glsl + set glslfd [open $glslfile w]; puts $glslfd $glsl; close $glslfd + split [string map {\n ""} [exec glslc -fshader-stage=$stage -mfmt=num -o - $glslfile]] "," +} + +set toyPushConstantsBlock { + layout(push_constant) uniform Args { + vec3 iResolution; + float iTime; + } args; +} + +When /someone/ wishes /p/ draws toy shader /shaderCode/ { + binary scan [$sha1Lib sha1 $shaderCode] H* sha1 + + set vertGlsl [csubst { + #version 450 + + $toyPushConstantsBlock + + layout(location = 0) out vec2 vUv; + + void main() { + vec2 vertices[6] = vec2[6]( + vec2(0.0, 0.0), vec2(1.0, 0.0), vec2(1.0, 1.0), + vec2(0.0, 0.0), vec2(1.0, 1.0), vec2(0.0, 1.0) + ); + vec2 uv = vertices[gl_VertexIndex]; + vUv = uv; + gl_Position = vec4(uv * 2.0 - 1.0, 0.0, 1.0); + } + }] + + set fragGlsl [csubst { + #version 450 + + $toyPushConstantsBlock + + layout(location = 0) in vec2 vUv; + layout(location = 0) out vec4 outColor; + + // Expose ShaderToy's built-in uniforms by the names user code expects. + #define iResolution args.iResolution + #define iTime args.iTime + + $shaderCode + + void main() { + vec2 fragCoord = vUv * iResolution.xy; + vec4 fragColor = vec4(0.0); + mainImage(fragColor, fragCoord); + // ShaderToy convention ignores alpha; force opaque. + outColor = vec4(fragColor.rgb, 1.0); + } + }] + + set vertShaderModule [$pipelineLib createShaderModule [toyGlslc vert $vertGlsl]] + set fragShaderModule [$pipelineLib createShaderModule [toyGlslc frag $fragGlsl]] + + set pipeline [$pipelineLib createPipeline \ + $vertShaderModule $fragShaderModule \ + $toyEncoder $toyArgsSize] + + Claim the GPU compiles pipeline $sha1 to $pipeline + + set startTime [/ [clock microseconds] 1000000.0] + When $p has canvas /id/ with /...wiOptions/ &\ + the clock time is /t/ { + set wiResolution [list [dict get $wiOptions width] [dict get $wiOptions height]] + Wish the GPU draws pipeline $sha1 onto canvas $id \ + with arguments [list $wiResolution [- $t $startTime]] + } +} + +}