From 82bd592e63acc7c10315c3e28483ead42cef9bbb Mon Sep 17 00:00:00 2001 From: treeform Date: Mon, 25 May 2026 10:49:04 -0700 Subject: [PATCH 1/8] Support dx and metal. --- README.md | 25 +- shady.nimble | 11 +- src/shady.nim | 1186 +---------------------------- src/shady/backends/dx12.nim | 304 ++++++++ src/shady/backends/glsl.nim | 101 +++ src/shady/backends/glsl3.nim | 11 + src/shady/backends/glsl4.nim | 3 + src/shady/backends/metal4.nim | 253 ++++++ src/shady/backends/shared.nim | 1353 +++++++++++++++++++++++++++++++++ tests/test_backends.nim | 157 ++++ tests/test_cpu.nim | 77 ++ tests/test_directx.nim | 380 +++++++++ tests/test_metal.nim | 45 ++ tests/test_opengl_glsl3.nim | 166 ++++ tests/test_opengl_glsl4.nim | 166 ++++ 15 files changed, 3048 insertions(+), 1190 deletions(-) create mode 100644 src/shady/backends/dx12.nim create mode 100644 src/shady/backends/glsl.nim create mode 100644 src/shady/backends/glsl3.nim create mode 100644 src/shady/backends/glsl4.nim create mode 100644 src/shady/backends/metal4.nim create mode 100644 src/shady/backends/shared.nim create mode 100644 tests/test_backends.nim create mode 100644 tests/test_cpu.nim create mode 100644 tests/test_directx.nim create mode 100644 tests/test_metal.nim create mode 100644 tests/test_opengl_glsl3.nim create mode 100644 tests/test_opengl_glsl4.nim diff --git a/README.md b/README.md index 72d02c2..eb8a536 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,27 @@ Currently supported shader types: * Vertex shaders paired with fragment shaders for the traditional graphics pipeline. * Compute shaders for GPU data processing. -Current GLSL targets: - -* Desktop GLSL via `glslDesktop` for OpenGL 4.1+. -* GLSL ES 3.0 via `glslES3` for OpenGL ES 3.0 / WebGL 2.0. +Current shader targets: + +* Desktop GLSL 4.10 via `glsl4Desktop` / `glslDesktop` for OpenGL 4.1+. +* Desktop GLSL 3.30 via `glsl3Desktop` for older desktop OpenGL tests. +* GLSL ES 3.0 via `glsl3WebGL` / `glslES3` for OpenGL ES 3.0 / WebGL 2.0. +* HLSL via `hlslDX12` for DirectX 12. +* Metal Shading Language via `metalMSL`. + +Use `toShader(shaderProc, target, stage)` for the general backend switch, or +`toGLSL`, `toHLSL`, and `toMSL` for language-specific helpers. + +Runtime backend tests are separate so platform graphics dependencies stay +optional: + +```sh +nimble test +nim c -r -d:shadyRunOpenGL tests/test_opengl_glsl3.nim +nim c -r -d:shadyRunOpenGL tests/test_opengl_glsl4.nim +nim c -r --path:C:\p\dx12\src -d:shadyRunDx12 tests/test_directx.nim +nim c -r --path:/path/to/metal4/src -d:shadyRunMetal tests/test_metal.nim +``` Shady uses: * `pixie` library for image operations. diff --git a/shady.nimble b/shady.nimble index f12faf3..97779c1 100644 --- a/shady.nimble +++ b/shady.nimble @@ -5,6 +5,11 @@ license = "MIT" srcDir = "src" -requires "nim >= 1.1.4" -requires "vmath >= 2.0.1" -requires "pixie >= 5.1.0" +requires "nim >= 1.1.4" +requires "vmath >= 2.0.1" +requires "pixie >= 5.1.0" + +task test, "Run Shady's stable CPU and codegen tests": + exec "nim c -r tests/test_shady.nim" + exec "nim c -r tests/test_cpu.nim" + exec "nim c -r tests/test_backends.nim" diff --git a/src/shady.nim b/src/shady.nim index 4f538c4..d21d82d 100644 --- a/src/shady.nim +++ b/src/shady.nim @@ -1,1185 +1,5 @@ -## Shader macro, converts Nim code into GLSL +## Public switchboard for Shady's shader backends. -import macros, pixie, strutils, tables, vmath -from chroma import ColorRGBX +import shady/backends/[shared, glsl, glsl3, glsl4, dx12, metal4] -type - GlslTarget* = enum - glslDesktop ## OpenGL 4.1+ (Mac, Linux, Windows) - glslES3 ## OpenGL ES 3.0 / WebGL 2.0 (Emscripten) - -var useResult {.compiletime.}: bool -var glslTarget* {.compiletime.}: GlslTarget - -proc err(msg: string, n: NimNode) {.noreturn.} = - error("[GLSL] " & msg, n) - -proc typeRename(t: string): string = - ## Some GLSL type names don't match Nim names, rename here. - case t - of "Mat2": "mat2" - of "Mat3": "mat3" - of "Mat4": "mat4" - - of "Vec2": "vec2" - of "Vec3": "vec3" - of "Vec4": "vec4" - - of "IVec2": "ivec2" - of "IVec3": "ivec3" - of "IVec4": "ivec4" - - of "UVec2": "uvec2" - of "UVec3": "uvec3" - of "UVec4": "uvec4" - - of "DVec2": "dvec2" - of "DVec3": "dvec3" - of "DVec4": "dvec4" - - of "int32": "int" - of "uint32": "uint" - - of "float32": "float" - of "float64": "float" - of "Uniform": "uniform" - of "UniformWriteOnly": "writeonly uniform" - of "Attribute": "attribute" - of "ColorRGBX": "vec4" - - of "SamplerBuffer": "samplerBuffer" - of "Sampler2d": "sampler2D" - of "USampler2d": "usampler2D" - of "Sampler2dArray": "sampler2DArray" - of "ImageBuffer": "imageBuffer" - of "UImageBuffer": "uimageBuffer" - else: t - -proc typeString(n: NimNode): string = - if n.kind != nnkBracketExpr: - typeRename(n.strVal) - else: - case n.repr: - of "GMat2[float32]": "mat2" - of "GMat3[float32]": "mat3" - of "GMat4[float32]": "mat4" - of "GVec2[float32]": "vec2" - of "GVec3[float32]": "vec3" - of "GVec4[float32]": "vec4" - of "GMat2[float64]": "dmat2" - of "GMat3[float64]": "dmat3" - of "GMat4[float64]": "dmat4" - of "GVec2[float64]": "dvec2" - of "GVec3[float64]": "dvec3" - of "GVec4[float64]": "dvec4" - of "GVec2[uint32]": "uvec2" - of "GVec3[uint32]": "uvec3" - of "GVec4[uint32]": "uvec4" - of "GVec2[int32]": "ivec2" - of "GVec3[int32]": "ivec3" - of "GVec4[int32]": "ivec4" - of "Uniform[float32]": "float" - of "Uniform[int]": "int" - else: - if n[0].repr == "array": - var length = 0 - if n[1].kind == nnkIntLit: - length = n[1].intVal.int - elif n[1].kind == nnkBracketExpr and n[1].len == 3 and n[1][0].repr == "..": - # array[0..1, T] case - length = n[1][2].intVal.int - n[1][1].intVal.int + 1 - elif n[1].kind == nnkInfix and n[1].len == 3 and n[1][0].repr == "..": - # array[0..1, T] case - length = n[1][2].intVal.int - n[1][1].intVal.int + 1 - - if length == 2: - if n[2].repr == "uint16": return "vec2" - if n[2].repr in ["uint32"]: return "uvec2" - if n[2].repr in ["int16", "int32"]: return "ivec2" - if n[2].repr in ["float32", "float64"]: return "vec2" - elif length == 3: - if n[2].repr == "uint16": return "vec3" - if n[2].repr in ["uint32"]: return "uvec3" - if n[2].repr in ["int16", "int32"]: return "ivec3" - if n[2].repr in ["float32", "float64"]: return "vec3" - elif length == 4: - if n[2].repr == "uint16": return "vec4" - if n[2].repr in ["uint32"]: return "uvec4" - if n[2].repr in ["int16", "int32"]: return "ivec4" - if n[2].repr in ["float32", "float64"]: return "vec4" - - err "can't figure out type: " & n.repr, n - -## Default constructor for different GLSL types. -proc typeDefault(t: string, n: NimNode): string = - case t - of "mat2": "mat2(0.0)" - of "mat3": "mat3(0.0)" - of "mat4": "mat4(0.0)" - of "vec4": "vec4(0.0)" - of "vec3": "vec3(0.0)" - of "vec2": "vec2(0.0)" - - of "uvec2": "uvec2(0)" - of "uvec3": "uvec3(0)" - of "uvec4": "uvec4(0)" - of "ivec2": "ivec2(0)" - of "ivec3": "ivec3(0)" - of "ivec4": "ivec4(0)" - - of "float": "0.0" - of "int": "0" - else: - err "no typeDefault " & t, n - -const glslGlobals = [ - "gl_Position", "gl_FragCoord", "gl_GlobalInvocationID", - "gl_VertexID", -] - -## List of function that GLSL provides, don't include their Nim src. -const glslFunctions = [ - "bool", "array", - "vec2", "vec3", "vec4", "mat2", "mat3", "mat4", - "Vec2", "Vec3", "Vec4", "mat2", "Mat3", "Mat4", - "uvec2", "uvec3", "uvec4", - "UVec2", "UVec3", "UVec4", - "ivec2", "ivec3", "ivec4", - "IVec2", "IVec3", "IVec4", - - "abs", "clamp", "min", "max", "dot", "sqrt", "mix", "length", - "texelFetch", "imageStore", "imageLoad", "texture", "textureSize", - "textureGrad", - "normalize", - "floor", "ceil", "round", "exp", "inversesqrt", - "[]", "[]=", - "inverse", - "sin", "cos", "tan", "pow", - "fmod", - "lessThan", "lessThanEqual", "greaterThan", "greaterThanEqual", - "equal", "notEqual", - "dFdx", "dFdy", "fract", "fwidth", - "smoothstep", "inc", "dec", "discardFragment" -] - -## Simply SKIP these functions. -const ignoreFunctions = [ - "echo", "print", "debugEcho", "$" -] - -proc isVectorAccess(s: string): bool = - ## is it a x,y,z or swizzle rgba= - for flavor in ["xyzw", "rgba", "stpq"]: - if s[0] in flavor: - for i, c in s: - if c notin flavor: - if c == '=' and i == s.len - 1: - return true - else: - return false - return true - return false - -proc procRename(t: string): string = - ## Some GLSL proc names don't match Nim names, rename here. - case t - of "not": "!" - of "and": "&&" - of "or": "||" - of "fmod": "mod" - of "mod": "%" - of "div": "/" - else: t.replace("`", "_") - -proc opPrecedence(op: string): int = - ## Given an operator return its precedence. - ## Used to decide if () are needed. - ## See: https://learnwebgl.brown37.net/12_shader_language/glsl_mathematical_operations.html - case op: - of "*", "/": 4 - of "+", "-": 5 - of "<", ">", "<=", ">=": 7 - of "==", "!=": 8 - of "&&": 12 - of "^^": 13 - of "||": 14 - of "=", "+=", "-=", "*=", "/=": 16 - else: -1 - -proc getPrecedence(n: NimNode): int = - ## Return the opPrecedence of the node operator or -1. - if n.kind == nnkInfix: - n[0].strVal.opPrecedence() - else: - -1 - -proc addIndent(res: var string, level: int) = - ## Add indent (only if its needed). - var - idx = res.len - 1 - spaces = 0 - while res[idx] == ' ': - dec idx - inc spaces - if spaces == 0 and res[idx] != '\n': - res.add '\n' - let level = level - spaces div 2 - for i in 0 ..< level: - res.add " " - -proc addGap(res: var string) = - if res.len > 0: - if not res.endsWith("\n\n"): - if res.endsWith("\n"): - res.add "\n" - else: - res.add "\n\n" - -proc addSmart(res: var string, c: char, others = {'}'}) = - ## Ads a char but first checks if its already here. - var idx = res.len - 1 - while res[idx] in Whitespace: - dec idx - if res[idx] != c and res[idx] notin others: - res.add c - -proc toCodeStmts(n: NimNode, res: var string, level = 0) - -proc isIntegerType(t: string): bool = - t in ["int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64"] - -proc toCode(n: NimNode, res: var string, level = 0) = - ## Inner code block. - - case n.kind - - of nnkAsgn: - res.addIndent level - n[0].toCode(res) - res.add " = " - n[1].toCode(res) - res.addSmart ';' - - of nnkInfix: - if n[0].repr in ["mod"] and not isIntegerType(n[1].getType().repr): - # In Nim float mod and integer made are same thing. - # In GLSL mod(float, float) is a function while % is for integers. - res.add n[0].repr - res.add "(" - n[1].toCode(res) - res.add ", " - n[2].toCode(res) - res.add ")" - - elif n[0].repr in ["+=", "-=", "*=", "/="]: - res.addIndent level - n[1].toCode(res) - res.add " " - n[0].toCode(res) - res.add " " - n[2].toCode(res) - res.addSmart ';' - - else: - let - a = n.getPrecedence() - l = n[1].getPrecedence() - r = n[2].getPrecedence() - if l >= a: - res.add "(" - n[1].toCode(res) - res.add ")" - else: - n[1].toCode(res) - res.add " " - n[0].toCode(res) - res.add " " - if r >= a: - res.add "(" - n[2].toCode(res) - res.add ")" - else: - n[2].toCode(res) - - of nnkHiddenDeref, nnkHiddenAddr: - n[0].toCode(res) - - of nnkCall, nnkCommand: - if n[0].strVal == "discardFragment": - # Special case for discarding the fragment. - # Because both Nim discard and GLSL discard are keywords that do different things. - res.add "discard" - return - if n[0].strVal == "inc": - n[1].toCode(res) - if n.len == 3: - res.add " += " - n[2].toCode(res) - else: - res.add "++" - return - if n[0].strVal == "dec": - n[1].toCode(res) - if n.len == 3: - res.add " -= " - n[2].toCode(res) - else: - res.add "--" - return - var procName = procRename(n[0].strVal) - if procName in ignoreFunctions: - return - if procName == "[]=": - n[1].toCode(res) - for i in 2 ..< n.len - 1: - res.add "[" - n[i].toCode(res) - res.add "]" - res.add " = " - n[n.len - 1].toCode(res) - res.addSmart ';' - - elif procName == "[]": - n[1].toCode(res) - for i in 2 ..< n.len: - res.add "[" - n[i].toCode(res) - res.add "]" - - elif isVectorAccess(procName): - if n[1].kind == nnkSym: - n[1].toCode(res) - else: - res.add "(" - n[1].toCode(res) - res.add ")" - res.add "." - res.add procName.replace("=", " = ") - if n.len == 3: - n[2].toCode(res) - else: - res.add procName - res.add "(" - for j in 1 ..< n.len: - if j != 1: res.add ", " - n[j].toCode(res) - res.add ")" - - of nnkDotExpr: - n[0].toCode(res) - res.add "." - n[1].toCode(res) - - of nnkBracketExpr: - if n[0].len == 2 and n[0][1].repr == "arr": - # Fastest vmath translates `obj.x` to `obj.arr[x]` for speed. - # Translate expanded the `obj.arr[x]` back to `.x` for shader. - let field = case n[1].repr: - of "0": ".x" - of "1": ".y" - of "2": ".z" - of "3": ".w" - else: "[" & n[1].repr & "]" - n[0][0].toCode(res) - res.add field - else: - n[0].toCode(res) - res.add "[" - n[1].toCode(res) - res.add "]" - - of nnkIdent, nnkSym: - res.add procRename(n.strVal) - - of nnkStmtListExpr: - for j in 0 ..< n.len: - n[j].toCode(res, level) - - of nnkStmtList: - for j in 0 ..< n.len: - if n[j].kind in [nnkCall, nnkCommand]: - res.addIndent level - n[j].toCode(res, level) - if n[j].kind notin [nnkLetSection, nnkVarSection, nnkCommentStmt]: - res.addSmart ';' - res.add "\n" - - of nnkIfStmt: - res.addIndent level - res.add "if (" - n[0][0].toCode(res) - res.add ") {\n" - n[0][1].toCodeStmts(res, level + 1) - res.addIndent level - res.add "}" - var i = 1 - while n.len > i: - if n[i].kind == nnkElse: - res.add " else {\n" - n[i][0].toCodeStmts(res, level + 1) - res.addIndent level - res.add "}" - elif n[i].kind == nnkElifBranch: - res.add " else if (" - n[i][0].toCode(res) - res.add ") {\n" - n[i][1].toCodeStmts(res, level + 1) - res.addIndent level - res.add "}" - else: - err "Not supported if branch", n - inc i - - # of nnkIfExpr: - # res.add "(" - # n[0][0].toCode(res) - # res.add ") ? (" - # n[1][0].toCode(res) - # res.add ") : (" - # n[2][0].toCode(res) - # res.add ")" - - of nnkConv: - res.add typeRename(n[0].strVal) - res.add "(" - n[1].toCode(res) - res.add ")" - - of nnkHiddenStdConv: - var typeStr = typeRename(n.getType.repr) - if typeStr.startsWith("range["): - n[1].toCode(res) - elif typeStr == "float" and n[1].kind == nnkIntLit: - res.add $n[1].intVal.float64 - elif typeStr == "float" and n[1].kind == nnkFloatLit: - res.add $n[1].floatVal.float64 - elif typeStr in ["int", "uint"] and n[1].kind in {nnkIntLit .. nnkInt64Lit}: - n[1].toCode(res) - else: - for j in 1 .. n.len-1: - res.add typeStr - res.add "(" - n[j].toCode(res) - res.add ")" - - of nnkNone: - assert false - - of nnkEmpty, nnkNilLit, nnkDiscardStmt, nnkPragma: - # Skip all nil, empty and discard statements. - discard - - of nnkIntLit .. nnkInt64Lit: - var iv = $n.intVal - res.add iv - - of nnkFloatLit .. nnkFloat64Lit: - var fv = $n.floatVal - res.add fv - - of nnkStrLit .. nnkTripleStrLit: - res.add $n.strVal.newLit.repr - - of nnkCommentStmt: - # preserve comments - for line in n.strVal.split("\n"): - res.addIndent level - res.add "// " - res.add line - res.add "\n" - - of nnkVarSection, nnkLetSection: - ## var and let ares the same in GLSL - for j in 0 ..< n.len: - res.addIndent level - n[j].toCode(res, level) - res.addSmart ';' - res.add "\n" - - of nnkIdentDefs: - for j in countup(0, n.len - 1, 3): - var typeStr = "" - if n[1].kind == nnkBracketExpr and - n[1][0].kind == nnkSym and - n[1][0].strVal == "array": - typeStr = typeRename(n[1][2].strVal) - typeStr.add "[" - typeStr.add n[1][1].repr - typeStr.add "]" - - res.add typeStr - res.add " " - n[0].toCode(res) - else: - typeStr = typeString(n[j].getTypeInst()) - res.add typeStr - res.add " " - n[j].toCode(res) - if n[j + 2].kind != nnkEmpty: - res.add " = " - n[j + 2].toCode(res) - else: - res.add " = " - res.add typeDefault(typeStr, n[j]) - - of nnkReturnStmt: - res.addIndent level - if n[0].kind == nnkAsgn: - n[0].toCode(res) - res.add "\n" - res.addIndent level - res.add "return result" - elif n[0].kind != nnkEmpty: - res.add "return " - n[0][1].toCode(res) - elif useResult: - res.add "return result" - else: - res.add "return" - - of nnkPrefix: - res.add procRename(n[0].strVal) & " (" - n[1].toCode(res) - res.add ")" - - of nnkWhileStmt: - res.addIndent level - res.add "while(" - n[0].toCode(res) - res.add ") {\n" - n[1].toCode(res, level + 1) - res.addIndent level - res.add "}" - - of nnkForStmt: - res.addIndent level - res.add "for(" - res.add "int " - res.add n[0].strVal - res.add " = " - n[1][1].toCode(res) - res.add "; " - res.add n[0].strVal - if n[1][0].strVal == "..<": - res.add " < " - elif n[1][0].strVal == "..": - res.add " <= " - else: - err "For loop only supports integer .. or ..<.", n - n[1][2].toCode(res) - res.add "; " - res.add n[0].strVal - res.add "++" - res.add ") {\n" - if n[2].kind == nnkStmtList: - n[2].toCode(res, level + 1) - else: - res.addIndent level - n[2].toCode(res, level + 1) - res.add ";" - res.addIndent level - res.add "}" - - of nnkBreakStmt: - res.addIndent level - res.add "break" - - of nnkProcDef: - err "Nested proc definitions are not allowed.", n - - of nnkCaseStmt: - res.addIndent level - res.add "switch(" - n[0].toCode(res) - res.add ") {\n" - for branch in n[1 .. ^1]: - if branch.kind == nnkOfBranch: - res.addIndent level - res.add "case " - branch[0].toCode(res) - res.add ":{\n" - branch[1].toCodeStmts(res, level + 1) - res.addIndent level - if branch[1].kind == nnkReturnStmt or branch[1].kind == nnkBreakStmt: - res.add "};\n" - else: - res.add "}; break;\n" - elif branch.kind == nnkElse: - res.addIndent level - res.add "default: {\n" - branch[0].toCodeStmts(res, level + 1) - res.addIndent level - if branch[0].kind == nnkReturnStmt or branch[0].kind == nnkBreakStmt: - res.add "};\n" - else: - res.add "}; break;\n" - else: - err "Can't compile branch", n - res.addIndent level - res.add "}" - - of nnkChckRange: - # skip check range and treat it as a hidden cast instead - var typeStr = typeRename(n.getType.repr) - res.add typeStr - res.add "(" - n[0].toCode(res) - res.add ")" - - of nnkObjConstr: - if repr(n[0][0]) == "[]": - # probably a swizzle call. - res.add n[1][1][1][1][0][0].strval - res.add "." - for part in n[1][1]: - res.add "xyzw"[part[1][1][1].intVal] - else: - echo n.treeRepr - err "Some sort of object constructor", n - - of nnkIfExpr: - var gotElse = false - res.add "(" - for subn in n: - case subn.kind - of nnkElifExpr: - if gotElse: - echo n.treeRepr - err "Cannot have elif after else", n - res.add "(" - subn[0].toCode(res) - res.add ") ? (" - subn[1].toCode(res) - res.add ") : " - of nnkElseExpr: - gotElse = true - res.add "(" - subn[0].toCode(res) - res.add ")" - else: - echo n.treeRepr - err "Invalid child of nnkIfExpr: " & subn.kind.repr, n - res.add ")" - if not gotElse: - echo n.treeRepr - err "nnkIfExpr is missing an else clause", n - - else: - echo n.treeRepr - err "Can't compile", n - -proc toCodeStmts(n: NimNode, res: var string, level = 0) = - if n.kind != nnkStmtList: - res.addIndent level - n.toCode(res, level) - res.addSmart ';' - res.add "\n" - else: - n.toCode(res, level) - -proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = - ## Top level block such as in and out params. - ## Generates the main function (which is not like all the other functions) - - assert topLevelNode.kind == nnkProcDef - - for n in topLevelNode: - case n.kind - of nnkEmpty: - discard - of nnkSym: - discard - of nnkFormalParams: - ## Main function parameters are different in they they go in as globals. - res.addGap() - for paramDefs in n: - if paramDefs.kind == nnkIdentDefs: - let typeNode = paramDefs[^2] - for i in 0 ..< paramDefs.len - 2: - let param = paramDefs[i] - if param.strVal in ["gl_FragColor", "gl_Position"]: - continue - if typeNode.kind == nnkVarTy: - if typeNode[0].repr == "seq": - res.add "buffer?" - res.add typeNode.repr - continue - elif typeNode[0].repr == "int": - res.add "flat " - res.add "out " - res.add typeRename(typeNode[0].strVal) - else: - if typeNode.kind == nnkBracketExpr: - res.add "in " - res.add typeString(typeNode) - else: - if glslTarget != glslES3 and param.strVal == "gl_FragCoord": - res.add "layout(origin_upper_left) " - if typeNode.strVal == "int": - res.add "flat " - res.add "in " - res.add typeRename(typeNode.strVal) - res.add " " - res.add param.strVal - res.addSmart ';' - res.add "\n" - else: - res.addGap() - res.add "void main() {\n" - n.toCodeStmts(res, level+1) - res.add "}\n" - -proc hasResult(node: NimNode): bool = - if node.kind == nnkSym and node.strVal == "result": - return true - for c in node.children: - if c.hasResult(): - return true - return false - -proc procDef(topLevelNode: NimNode): string = - ## Process whole function (that is not the main function). - - var procName = "" - var paramsStr = "" - var returnType = "void" - - assert topLevelNode.kind in {nnkFuncDef, nnkProcDef} - for n in topLevelNode: - case n.kind - of nnkEmpty, nnkPragma: - discard - of nnkSym: - procName = $n - of nnkFormalParams: - # Reading parameter list `(x, y, z: float)` - if n[0].kind != nnkEmpty: - returnType = typeRename(n[0].strVal) - for paramDef in n[1 .. ^1]: - # The paramDef is like `x, y, z: float`. - if paramDef.kind != nnkEmpty: - for param in paramDef[0 ..< ^2]: - # Process each `x`, `y`, `z` in a loop. - paramsStr.add " " - let paramName = param.repr() - let paramType = param.getTypeInst() - if paramType.kind == nnkVarTy: - # Process `x: var float` - if paramType[0].strVal == "int": - paramsStr.add "flat " - paramsStr.add "inout " - paramsStr.add typeRename(paramType[0].strVal) - elif paramType.kind == nnkBracketExpr: - # Process varying[uniform]. - # TODO test? - paramsStr.add paramType[0].strVal - paramsStr.add " " - paramsStr.add typeRename(paramType[1].strVal) - else: - # Just a simple `x: float` case. - if paramType.strVal == "int": - paramsStr.add "flat " - paramsStr.add typeRename(paramType.strVal) - paramsStr.add " " - paramsStr.add paramName - paramsStr.add ",\n" - else: - if paramsStr.len > 0: - paramsStr = paramsStr[0 .. ^3] & "\n" - result.add returnType & " " & procName & "(\n" & paramsStr & ") {\n" - useResult = n.hasResult() - if useResult: - result.addIndent(1) - result.add returnType - result.add " result;" - n.toCodeStmts(result, 1) - if useResult: - if "return result" notin result[^20..^1]: - result.addIndent(1) - result.add "return result;\n" - result.add "}" - -proc gatherTypes( - typeInst: NimNode, - types: var Table[string, string] -) = - ## Looks for types used and gathers their struct definitions. - var typeInst = typeInst - if typeInst.kind == nnkVarTy: - typeInst = typeInst[0] - if typeInst.kind == nnkBracketExpr: - for i in 1 ..< typeInst.len: - gatherTypes(typeInst[i], types) - return - - if typeInst.kind == nnkSym: - let name = typeInst.strVal - if name in types: return - if typeRename(name) != name: return - if name in ["ColorRGBX"]: - # Special case for ColorRGBX which is handled as vec4. - return - - let impl = typeInst.getImpl() - if impl.kind == nnkTypeDef and impl[2].kind == nnkObjectTy: - var def = "struct " & name & " {\n" - let obj = impl[2] - let reclist = obj[2] - for field in reclist: - if field.kind == nnkIdentDefs: - let fieldTypeNode = field[^2] - let fieldType = fieldTypeNode.getTypeInst() - gatherTypes(fieldType, types) - for i in 0 ..< field.len - 2: - var fieldName = field[i].repr - if fieldName.endsWith("*"): - fieldName = fieldName[0 .. ^2] - def.add " " & typeString(fieldType) & " " & fieldName & ";\n" - def.add "};" - types[name] = def - -proc gatherFunction( - topLevelNode: NimNode, - functions: var Table[string, string], - globals: var Table[string, string], - types: var Table[string, string] -) = - - ## Looks for functions this function calls and brings them up - for n in topLevelNode: - if n.kind == nnkSym: - # Looking for globals. - let name = n.strVal - if name notin glslGlobals and name notin glslFunctions and name notin globals: - if n.owner().symKind == nskModule: - let impl = n.getImpl() - if impl.kind notin { - nnkTypeDef, - nnkIteratorDef, - nnkProcDef, - nnkFuncDef - } and impl.kind != nnkNilLit: - var defStr = "" - let typeInst = n.getTypeInst - gatherTypes(typeInst, types) - if typeInst.kind == nnkBracketExpr: - # might be a uniform - if typeInst[0].repr in ["Uniform", "UniformWriteOnly", "Attribute"]: - defStr.add typeRename(typeInst[0].repr) - defStr.add " " - let samplerType = typeString(typeInst[1]) - if glslTarget == glslES3 and samplerType in [ - "sampler2D", "usampler2D", "sampler2DArray", - "samplerBuffer", "imageBuffer", "uimageBuffer"]: - defStr.add "highp " - defStr.add samplerType - elif typeInst[0].repr == "array": - defStr.add typeString(typeInst[2]) - defStr.add "[" - defStr.add typeRename(typeInst[1][2].repr) - defStr.add "]" - else: - err "Invalid x[y].", n - else: - defStr.add typeRename(typeInst.repr) - defStr.add " " & name - if impl[2].kind != nnkEmpty: - defStr.add " = " & repr(impl[2]) - defStr.addSmart ';' - if defStr notin ["uniform Uniform = T;", - "attribute Attribute = T;"]: - globals[name] = defStr - - if n.kind == nnkCall: - # Looking for functions. - if repr(n[0]) == "[]": - continue - let procName = repr n[0] - if procName in ignoreFunctions: - continue - if procName notin glslFunctions and - procName notin functions and - not isVectorAccess(procName): - ## If its not a builtin proc, we need to bring definition. - let impl = n[0].getImpl() - gatherFunction(impl, functions, globals, types) - functions[procName] = procDef(impl) - - if n.kind == nnkFormalParams: - for i in 1 ..< n.len: - let paramDef = n[i] - let paramType = paramDef[^2].getTypeInst() - gatherTypes(paramType, types) - - gatherFunction(n, functions, globals, types) - -proc toGLSLInner*(s: NimNode, version, extra: string): string = - - var code: string - - # Add GLS header stuff. - code.add "#version " & version & "\n" - code.add extra - code.add "// from " & s.strVal & "\n" - - var n = getImpl(s) - - # Gather all globals and functions, and globals and functions they use. - var functions: Table[string, string] - var globals: Table[string, string] - var types: Table[string, string] - gatherFunction(n, functions, globals, types) - - # Put types first. - code.addGap() - for k, v in types: - code.add(v) - code.add "\n" - - # Put globals next. - code.addGap() - for k, v in globals: - code.add(v) - code.add "\n" - - # Put functions definition (just name and types part). - code.addGap() - for k, v in functions: - var funCode = v.split(" {")[0] - funCode = funCode - .replace("\n", "") - .replace(" ", " ") - .replace(", ", ", ") - .replace("( ", "(") - code.add funCode - code.addSmart ';' - code.add "\n" - - # Put functions (with bodies) next. - code.addGap() - for k, v in functions: - code.add v - code.add "\n" - - # Put the main function last. - toCodeTopLevel(n, code) - - return code - -proc toGLSLInner*(s: NimNode, target: GlslTarget): string = - ## Converts proc to a GLSL string for the given target. - glslTarget = target - case target - of glslDesktop: - toGLSLInner(s, "410", "") - of glslES3: - toGLSLInner(s, "300 es", "precision highp float;\n") - -macro toGLSL*( - s: typed, - target: static[GlslTarget] -): string = - ## Converts proc to a GLSL string for the given target. - newLit(toGLSLInner(s, target)) - -macro toGLSL*( - s: typed, - version = "410", - extra = "precision highp float;\n" -): string = - ## Converts proc to a glsl string. - ## For target-aware compilation, use the GlslTarget overload instead. - glslTarget = if "es" in version.strVal: glslES3 else: glslDesktop - newLit(toGLSLInner(s, version.strVal, extra.strVal)) - -## GLSL helper functions - -type - Uniform*[T] = T - UniformWriteOnly*[T] = T - Attribute*[T] = T - - SamplerBuffer* = object - data*: seq[float32] - - ImageBuffer* = object - image*: Image - - UImageBuffer* = object - image*: Image - - Sampler2d* = object - image*: Image - - USampler2d* = object - image*: Image - - Sampler2dArray* = object - images*: seq[Image] - -var - ## GLSL globals. - gl_Position*: Vec4 - gl_VertexID*: int32 - -proc texelFetch*(buffer: Uniform[SamplerBuffer], index: SomeInteger): Vec4 = - vec4(buffer.data[index.int], 0, 0, 0) - -proc texelFetch*(buffer: Uniform[Sampler2D], pos: IVec2, level: int): Vec4 = - let c = buffer.image[pos.x.int, pos.y.int] - return vec4(c.r.float32/255, c.g.float32/255, c.b.float32/255, c.a.float32/255) - -proc texelFetch*(buffer: Uniform[USampler2D], pos: IVec2, level: int): UVec4 = - ## CPU stub for usampler2D; not used at runtime. Returns zeros. - uvec4(0u32, 0u32, 0u32, 0u32) - -proc imageLoad*( - buffer: var UniformWriteOnly[UImageBuffer], index: int32 -): UVec4 = - result.x = buffer.image.data[index.int].r - result.g = buffer.image.data[index.int].g - result.b = buffer.image.data[index.int].b - result.a = buffer.image.data[index.int].a - -proc imageStore*(buffer: var UniformWriteOnly[UImageBuffer], index: int32, - color: UVec4) = - buffer.image.data[index.int].r = clamp(color.x, 0, 255).uint8 - buffer.image.data[index.int].g = clamp(color.y, 0, 255).uint8 - buffer.image.data[index.int].b = clamp(color.z, 0, 255).uint8 - buffer.image.data[index.int].a = clamp(color.w, 0, 255).uint8 - -proc imageStore*(buffer: var UniformWriteOnly[ImageBuffer], index: int32, - color: Vec4) = - buffer.image.data[index.int].r = clamp(color.x*255, 0, 255).uint8 - buffer.image.data[index.int].g = clamp(color.y*255, 0, 255).uint8 - buffer.image.data[index.int].b = clamp(color.z*255, 0, 255).uint8 - buffer.image.data[index.int].a = clamp(color.w*255, 0, 255).uint8 - -proc imageStore*(buffer: var Uniform[Sampler2D], pos: IVec2, - color: Vec4) = - buffer.image[pos.x.int, pos.y.int] = rgbx( - clamp(color.x*255, 0, 255).uint8, - clamp(color.y*255, 0, 255).uint8, - clamp(color.z*255, 0, 255).uint8, - clamp(color.w*255, 0, 255).uint8, - ) - -proc vec4*(c: ColorRGBX): Vec4 = - vec4( - c.r.float32/255, - c.g.float32/255, - c.b.float32/255, - c.a.float32/255 - ) - -proc dFdx*(a: float32): float32 = - raise newException(Exception, "dFdx is not implemented") - -proc dFdx*(a: Vec2): Vec2 = - raise newException(Exception, "dFdx is not implemented") - -proc dFdx*(a: Vec3): Vec3 = - raise newException(Exception, "dFdx is not implemented") - -proc dFdx*(a: Vec4): Vec4 = - raise newException(Exception, "dFdx is not implemented") - -proc dFdy*(a: float32): float32 = - raise newException(Exception, "dFdy is not implemented") - -proc dFdy*(a: Vec2): Vec2 = - raise newException(Exception, "dFdy is not implemented") - -proc dFdy*(a: Vec3): Vec3 = - raise newException(Exception, "dFdy is not implemented") - -proc dFdy*(a: Vec4): Vec4 = - raise newException(Exception, "dFdy is not implemented") - -proc fwidth*(a: float32): float32 = - raise newException(Exception, "fwidth is not implemented") - -proc fwidth*(a: Vec2): Vec2 = - raise newException(Exception, "fwidth is not implemented") - -proc fwidth*(a: Vec3): Vec3 = - raise newException(Exception, "fwidth is not implemented") - -proc fwidth*(a: Vec4): Vec4 = - raise newException(Exception, "fwidth is not implemented") - -proc fmod*(x, y: float32): float32 = - x - y * floor(x / y) - -proc fmod*(x: Vec2, y: float32): Vec2 = - vec2(fmod(x.x, y), fmod(x.y, y)) - -proc fmod*(x: Vec3, y: float32): Vec3 = - vec3(fmod(x.x, y), fmod(x.y, y), fmod(x.z, y)) - -proc fmod*(x: Vec4, y: float32): Vec4 = - vec4(fmod(x.x, y), fmod(x.y, y), fmod(x.z, y), fmod(x.w, y)) - -proc fmod*(x, y: Vec2): Vec2 = - vec2(fmod(x.x, y.x), fmod(x.y, y.y)) - -proc fmod*(x, y: Vec3): Vec3 = - vec3(fmod(x.x, y.x), fmod(x.y, y.y), fmod(x.z, y.z)) - -proc fmod*(x, y: Vec4): Vec4 = - vec4(fmod(x.x, y.x), fmod(x.y, y.y), fmod(x.z, y.z), fmod(x.w, y.w)) - -proc fract*(a: float32): float32 = - a - floor(a) - -proc fract*(a: Vec2): Vec2 = - vec2(fract(a.x), fract(a.y)) - -proc fract*(a: Vec3): Vec3 = - vec3(fract(a.x), fract(a.y), fract(a.z)) - -proc fract*(a: Vec4): Vec4 = - vec4(fract(a.x), fract(a.y), fract(a.z), fract(a.w)) - -proc smoothstep*(a, b, x: float32): float32 = - let t = clamp((x - a) / (b - a), 0.0, 1.0) - t * t * (3.0 - 2.0 * t) - -proc smoothstep*(a, b, x: Vec2): Vec2 = - vec2(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y)) - -proc smoothstep*(a, b, x: Vec3): Vec3 = - vec3(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y), smoothstep(a.z, b.z, x.z)) - -proc smoothstep*(a, b, x: Vec4): Vec4 = - vec4(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y), smoothstep(a.z, b.z, x.z), smoothstep(a.w, b.w, x.w)) - -proc texture*(buffer: Uniform[Sampler2D], pos: Vec2): Vec4 = - let pos = pos - vec2(0.5 / buffer.image.width.float32, 0.5 / - buffer.image.height.float32) - buffer.image.getRgbaSmooth( - ((pos.x mod 1.0) * buffer.image.width.float32), - ((pos.y mod 1.0) * buffer.image.height.float32) - ).vec4() - -proc textureSize*(buffer: Uniform[Sampler2D], level: int): Vec2 = - vec2(buffer.image.width.float32, buffer.image.height.float32) - -proc textureGrad*( - s: Uniform[Sampler2D], - uv: Vec3, - dUVdx: Vec2, - dUVdy: Vec2 -): Vec4 = - texture(s, uv.xy) - -proc textureGrad*( - s: Uniform[Sampler2DArray], - uvw: Vec3, - dUVdx: Vec2, - dUVdy: Vec2 -): Vec4 = - vec4(0, 0, 0, 0) - -proc discardFragment*() = - discard +export shared, glsl, glsl3, glsl4, dx12, metal4 diff --git a/src/shady/backends/dx12.nim b/src/shady/backends/dx12.nim new file mode 100644 index 0000000..34df59f --- /dev/null +++ b/src/shady/backends/dx12.nim @@ -0,0 +1,304 @@ +import strutils, tables + +type EntryParam* = tuple[name, typ: string, isOut: bool] + +const hlslHeader* = "// target hlsl dx12\n" + +proc hlslTypeRename*(t: string): string = + case t + of "Mat2": "float2x2" + of "Mat3": "float3x3" + of "Mat4": "float4x4" + + of "Vec2": "float2" + of "Vec3": "float3" + of "Vec4": "float4" + + of "IVec2": "int2" + of "IVec3": "int3" + of "IVec4": "int4" + + of "UVec2": "uint2" + of "UVec3": "uint3" + of "UVec4": "uint4" + + of "DVec2": "double2" + of "DVec3": "double3" + of "DVec4": "double4" + + of "int32": "int" + of "uint32": "uint" + + of "float32": "float" + of "float64": "double" + of "ColorRGBX": "float4" + + of "SamplerBuffer": "Buffer" + of "Sampler2d": "Texture2D" + of "USampler2d": "Texture2D" + of "Sampler2dArray": "Texture2DArray" + of "ImageBuffer": "RWBuffer" + of "UImageBuffer": "RWBuffer" + else: t + +proc hlslProcRename*(t: string): string = + case t + of "not": "!" + of "and": "&&" + of "or": "||" + of "vec2": "float2" + of "vec3": "float3" + of "vec4": "float4" + of "mat2": "float2x2" + of "mat3": "float3x3" + of "mat4": "float4x4" + of "uvec2": "uint2" + of "uvec3": "uint3" + of "uvec4": "uint4" + of "ivec2": "int2" + of "ivec3": "int3" + of "ivec4": "int4" + of "mix": "lerp" + of "fract": "frac" + of "dFdx": "ddx" + of "dFdy": "ddy" + of "fmod": "fmod" + of "mod": "%" + of "div": "/" + of "gl_Position": "gl_Position" + else: t.replace("`", "_") + +proc hlslTypeDefault*(t: string): string = + case t + of "float2x2": "float2x2(0.0)" + of "float3x3": "float3x3(0.0)" + of "float4x4": "float4x4(0.0)" + of "float4": "float4(0.0, 0.0, 0.0, 0.0)" + of "float3": "float3(0.0, 0.0, 0.0)" + of "float2": "float2(0.0, 0.0)" + + of "uint2": "uint2(0, 0)" + of "uint3": "uint3(0, 0, 0)" + of "uint4": "uint4(0, 0, 0, 0)" + of "int2": "int2(0, 0)" + of "int3": "int3(0, 0, 0)" + of "int4": "int4(0, 0, 0, 0)" + + of "float": "0.0" + of "double": "0.0" + of "int": "0" + of "uint": "0" + else: "" + +proc semanticBase(name: string): string = + let lower = name.toLowerAscii() + if name in ["gl_Position", "position"] or lower.endsWith("position"): + "POSITION" + elif name in ["gl_FragCoord"]: + "SV_POSITION" + elif "color" in lower or lower.endsWith("col"): + "COLOR" + elif "normal" in lower: + "NORMAL" + else: + "TEXCOORD" + +proc hlslSemantic*(name: string, index: int, isOutput: bool): string = + case name + of "fragColor", "gl_FragColor": + " : SV_TARGET" + of "gl_Position": + " : SV_POSITION" + of "gl_FragCoord": + " : SV_POSITION" + of "gl_VertexID": + " : SV_VertexID" + else: + let base = semanticBase(name) + if base.startsWith("SV_"): + " : " & base + else: + " : " & base & $index + +proc nextSemanticIndex( + name: string, + counts: var Table[string, int] +): int = + case name + of "fragColor", "gl_FragColor", "gl_Position", "gl_FragCoord", "gl_VertexID": + return 0 + else: + let base = semanticBase(name) + if base.startsWith("SV_"): + return 0 + result = counts.getOrDefault(base, 0) + counts[base] = result + 1 + +proc hlslOutputFieldName*(name: string): string = + if name == "gl_Position": "pos" else: name + +proc hlslSamplerDecl*(name: string): string = + "SamplerState " & name & "Sampler;" + +proc hlslResourceCall*( + name: string, + args: openArray[string], + firstArgIsSamplerBuffer: bool +): string = + case name + of "texture": + if args.len != 2: return "" + args[0] & ".Sample(" & args[0] & "Sampler, " & args[1] & ")" + of "texelFetch": + if args.len < 2: return "" + if firstArgIsSamplerBuffer: + args[0] & ".Load(" & args[1] & ")" + else: + let level = if args.len >= 3: args[2] else: "0" + args[0] & ".Load(int3(" & args[1] & ", " & level & "))" + of "imageLoad": + if args.len != 2: return "" + args[0] & "[" & args[1] & "]" + of "imageStore": + if args.len != 3: return "" + args[0] & "[" & args[1] & "] = " & args[2] + else: + "" + +proc firstOutput(params: openArray[EntryParam]): int = + result = -1 + for i, p in params: + if p.isOut: + return i + +proc hasParam(params: openArray[EntryParam], name: string): bool = + for p in params: + if p.name == name: + return true + +proc outputLocals(params: openArray[EntryParam], level: int): string = + let indent = repeat(" ", level) + for p in params: + if p.isOut: + result.add indent + result.add p.typ + result.add " " + result.add p.name + result.add " = " + result.add hlslTypeDefault(p.typ) + result.add ";\n" + +proc emitHlslEntry*( + params: openArray[EntryParam], + bodyCode: string, + stage: int +): string = + case stage + of 1: + var outputs: seq[EntryParam] + for p in params: + if p.isOut: + outputs.add p + + result.add "\nstruct VSOutput {\n" + var outputSemanticCounts: Table[string, int] + for p in outputs: + result.add " " + result.add p.typ + result.add " " + result.add hlslOutputFieldName(p.name) + result.add hlslSemantic( + p.name, + nextSemanticIndex(p.name, outputSemanticCounts), + true + ) + result.add ";\n" + result.add "};\n\n" + + result.add "VSOutput VSMain(" + var inputSemanticCounts: Table[string, int] + var first = true + for p in params: + if not p.isOut: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " " + result.add p.typ + result.add " " + result.add p.name + result.add hlslSemantic( + p.name, + nextSemanticIndex(p.name, inputSemanticCounts), + false + ) + if not params.hasParam("gl_VertexID") and "gl_VertexID" in bodyCode: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " uint gl_VertexID : SV_VertexID" + if not first: + result.add "\n" + result.add ") {\n" + result.add " VSOutput output;\n" + result.add outputLocals(params, 1) + result.add bodyCode + for p in outputs: + result.add " output." + result.add hlslOutputFieldName(p.name) + result.add " = " + result.add p.name + result.add ";\n" + result.add " return output;\n" + result.add "}\n" + + of 2: + let outputIndex = firstOutput(params) + let returnType = if outputIndex >= 0: params[outputIndex].typ else: "void" + result.add "\n" + result.add returnType + result.add " PSMain(" + var inputSemanticCounts: Table[string, int] + var first = true + for p in params: + if not p.isOut: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " " + result.add p.typ + result.add " " + result.add p.name + result.add hlslSemantic( + p.name, + nextSemanticIndex(p.name, inputSemanticCounts), + false + ) + if not first: + result.add "\n" + result.add ")" + if outputIndex >= 0: + result.add hlslSemantic(params[outputIndex].name, 0, true) + result.add " {\n" + result.add outputLocals(params, 1) + result.add bodyCode + if outputIndex >= 0: + result.add " return " + result.add params[outputIndex].name + result.add ";\n" + result.add "}\n" + + of 3: + result.add "\n[numthreads(1, 1, 1)]\n" + result.add "void CSMain(uint3 gl_GlobalInvocationID : SV_DispatchThreadID) {\n" + result.add bodyCode + result.add "}\n" + + else: + discard diff --git a/src/shady/backends/glsl.nim b/src/shady/backends/glsl.nim new file mode 100644 index 0000000..ddca541 --- /dev/null +++ b/src/shady/backends/glsl.nim @@ -0,0 +1,101 @@ +import strutils + +proc glslTypeRename*(t: string): string = + case t + of "Mat2": "mat2" + of "Mat3": "mat3" + of "Mat4": "mat4" + + of "Vec2": "vec2" + of "Vec3": "vec3" + of "Vec4": "vec4" + + of "IVec2": "ivec2" + of "IVec3": "ivec3" + of "IVec4": "ivec4" + + of "UVec2": "uvec2" + of "UVec3": "uvec3" + of "UVec4": "uvec4" + + of "DVec2": "dvec2" + of "DVec3": "dvec3" + of "DVec4": "dvec4" + + of "int32": "int" + of "uint32": "uint" + + of "float32": "float" + of "float64": "float" + of "Uniform": "uniform" + of "UniformWriteOnly": "writeonly uniform" + of "Attribute": "attribute" + of "ColorRGBX": "vec4" + + of "SamplerBuffer": "samplerBuffer" + of "Sampler2d": "sampler2D" + of "USampler2d": "usampler2D" + of "Sampler2dArray": "sampler2DArray" + of "ImageBuffer": "imageBuffer" + of "UImageBuffer": "uimageBuffer" + else: t + +proc glslProcRename*(t: string): string = + case t + of "not": "!" + of "and": "&&" + of "or": "||" + of "fmod": "mod" + of "mod": "%" + of "div": "/" + else: t.replace("`", "_") + +proc glslTypeDefault*(t: string): string = + case t + of "mat2": "mat2(0.0)" + of "mat3": "mat3(0.0)" + of "mat4": "mat4(0.0)" + of "vec4": "vec4(0.0)" + of "vec3": "vec3(0.0)" + of "vec2": "vec2(0.0)" + + of "uvec2": "uvec2(0)" + of "uvec3": "uvec3(0)" + of "uvec4": "uvec4(0)" + of "ivec2": "ivec2(0)" + of "ivec3": "ivec3(0)" + of "ivec4": "ivec4(0)" + + of "float": "0.0" + of "int": "0" + of "uint": "0" + else: "" + +const glslGlobals* = [ + "gl_Position", "gl_FragCoord", "gl_GlobalInvocationID", + "gl_VertexID", +] + +const glslFunctions* = [ + "bool", "array", + "vec2", "vec3", "vec4", "mat2", "mat3", "mat4", + "Vec2", "Vec3", "Vec4", "mat2", "Mat3", "Mat4", + "uvec2", "uvec3", "uvec4", + "UVec2", "UVec3", "UVec4", + "ivec2", "ivec3", "ivec4", + "IVec2", "IVec3", "IVec4", + + "abs", "clamp", "min", "max", "dot", "sqrt", "mix", "length", + "texelFetch", "imageStore", "imageLoad", "texture", "textureSize", + "textureGrad", + "normalize", + "floor", "ceil", "round", "exp", "inversesqrt", + "[]", "[]=", + "inverse", + "sin", "cos", "tan", "pow", + "fmod", + "lessThan", "lessThanEqual", "greaterThan", "greaterThanEqual", + "equal", "notEqual", + "dFdx", "dFdy", "fract", "fwidth", + "smoothstep", "inc", "dec", "discardFragment" +] diff --git a/src/shady/backends/glsl3.nim b/src/shady/backends/glsl3.nim new file mode 100644 index 0000000..a361db9 --- /dev/null +++ b/src/shady/backends/glsl3.nim @@ -0,0 +1,11 @@ +const + glsl3DesktopVersion* = "330" + glsl3DesktopExtra* = "" + glsl3WebGLVersion* = "300 es" + glsl3WebGLExtra* = "precision highp float;\n" + +proc glsl3NeedsHighp*(samplerType: string): bool = + samplerType in [ + "sampler2D", "usampler2D", "sampler2DArray", + "samplerBuffer", "imageBuffer", "uimageBuffer" + ] diff --git a/src/shady/backends/glsl4.nim b/src/shady/backends/glsl4.nim new file mode 100644 index 0000000..921c4e4 --- /dev/null +++ b/src/shady/backends/glsl4.nim @@ -0,0 +1,3 @@ +const + glsl4DesktopVersion* = "410" + glsl4DesktopExtra* = "" diff --git a/src/shady/backends/metal4.nim b/src/shady/backends/metal4.nim new file mode 100644 index 0000000..438a305 --- /dev/null +++ b/src/shady/backends/metal4.nim @@ -0,0 +1,253 @@ +import strutils + +type EntryParam* = tuple[name, typ: string, isOut: bool] + +const metalHeader* = "#include \nusing namespace metal;\n" + +proc metalTypeRename*(t: string): string = + case t + of "Mat2": "float2x2" + of "Mat3": "float3x3" + of "Mat4": "float4x4" + + of "Vec2": "float2" + of "Vec3": "float3" + of "Vec4": "float4" + + of "IVec2": "int2" + of "IVec3": "int3" + of "IVec4": "int4" + + of "UVec2": "uint2" + of "UVec3": "uint3" + of "UVec4": "uint4" + + of "DVec2": "double2" + of "DVec3": "double3" + of "DVec4": "double4" + + of "int32": "int" + of "uint32": "uint" + + of "float32": "float" + of "float64": "double" + of "ColorRGBX": "float4" + + of "SamplerBuffer": "texture_buffer" + of "Sampler2d": "texture2d" + of "USampler2d": "texture2d" + of "Sampler2dArray": "texture2d_array" + of "ImageBuffer": "device float4*" + of "UImageBuffer": "device uint4*" + else: t + +proc metalProcRename*(t: string): string = + case t + of "not": "!" + of "and": "&&" + of "or": "||" + of "vec2": "float2" + of "vec3": "float3" + of "vec4": "float4" + of "mat2": "float2x2" + of "mat3": "float3x3" + of "mat4": "float4x4" + of "uvec2": "uint2" + of "uvec3": "uint3" + of "uvec4": "uint4" + of "ivec2": "int2" + of "ivec3": "int3" + of "ivec4": "int4" + of "dFdx": "dfdx" + of "dFdy": "dfdy" + of "fmod": "fmod" + of "mod": "%" + of "div": "/" + else: t.replace("`", "_") + +proc metalTypeDefault*(t: string): string = + case t + of "float2x2": "float2x2(0.0)" + of "float3x3": "float3x3(0.0)" + of "float4x4": "float4x4(0.0)" + of "float4": "float4(0.0, 0.0, 0.0, 0.0)" + of "float3": "float3(0.0, 0.0, 0.0)" + of "float2": "float2(0.0, 0.0)" + + of "uint2": "uint2(0, 0)" + of "uint3": "uint3(0, 0, 0)" + of "uint4": "uint4(0, 0, 0, 0)" + of "int2": "int2(0, 0)" + of "int3": "int3(0, 0, 0)" + of "int4": "int4(0, 0, 0, 0)" + + of "float": "0.0" + of "double": "0.0" + of "int": "0" + of "uint": "0" + else: "" + +proc metalAttribute*(name: string, index: int, isOutput: bool): string = + case name + of "fragColor", "gl_FragColor": + " [[color(0)]]" + of "gl_Position": + " [[position]]" + of "gl_VertexID": + " [[vertex_id]]" + else: + if isOutput: + "" + else: + " [[attribute(" & $index & ")]]" + +proc metalOutputFieldName*(name: string): string = + if name == "gl_Position": "position" else: name + +proc metalResourceCall*( + name: string, + args: openArray[string], + firstArgIsSamplerBuffer: bool +): string = + case name + of "texture": + if args.len != 2: return "" + args[0] & ".sample(" & args[0] & "Sampler, " & args[1] & ")" + of "texelFetch": + if args.len < 2: return "" + if firstArgIsSamplerBuffer: + "float4(" & args[0] & ".read(uint(" & args[1] & ")), 0.0, 0.0, 0.0)" + else: + let level = if args.len >= 3: args[2] else: "0" + args[0] & ".read(uint2(" & args[1] & "), uint(" & level & "))" + of "imageLoad": + if args.len != 2: return "" + args[0] & "[" & args[1] & "]" + of "imageStore": + if args.len != 3: return "" + args[0] & "[" & args[1] & "] = " & args[2] + else: + "" + +proc firstOutput(params: openArray[EntryParam]): int = + result = -1 + for i, p in params: + if p.isOut: + return i + +proc hasParam(params: openArray[EntryParam], name: string): bool = + for p in params: + if p.name == name: + return true + +proc outputLocals(params: openArray[EntryParam], level: int): string = + let indent = repeat(" ", level) + for p in params: + if p.isOut: + result.add indent + result.add p.typ + result.add " " + result.add p.name + result.add " = " + result.add metalTypeDefault(p.typ) + result.add ";\n" + +proc emitMetalEntry*( + params: openArray[EntryParam], + bodyCode: string, + stage: int +): string = + case stage + of 1: + var outputs: seq[EntryParam] + for p in params: + if p.isOut: + outputs.add p + + result.add "\nstruct VertexOut {\n" + for p in outputs: + result.add " " + result.add p.typ + result.add " " + result.add metalOutputFieldName(p.name) + result.add metalAttribute(p.name, 0, true) + result.add ";\n" + result.add "};\n\n" + + result.add "vertex VertexOut vertexMain(" + var inputIndex = 0 + var first = true + for p in params: + if not p.isOut: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " " + result.add p.typ + result.add " " + result.add p.name + result.add metalAttribute(p.name, inputIndex, false) + inc inputIndex + if not params.hasParam("gl_VertexID") and "gl_VertexID" in bodyCode: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " uint gl_VertexID [[vertex_id]]" + if not first: + result.add "\n" + result.add ") {\n" + result.add " VertexOut output;\n" + result.add outputLocals(params, 1) + result.add bodyCode + for p in outputs: + result.add " output." + result.add metalOutputFieldName(p.name) + result.add " = " + result.add p.name + result.add ";\n" + result.add " return output;\n" + result.add "}\n" + + of 2: + let outputIndex = firstOutput(params) + let returnType = if outputIndex >= 0: params[outputIndex].typ else: "void" + result.add "\nfragment " + result.add returnType + result.add " fragmentMain(" + var inputIndex = 0 + var first = true + for p in params: + if not p.isOut: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " " + result.add p.typ + result.add " " + result.add p.name + result.add metalAttribute(p.name, inputIndex, false) + inc inputIndex + if not first: + result.add "\n" + result.add ") {\n" + result.add outputLocals(params, 1) + result.add bodyCode + if outputIndex >= 0: + result.add " return " + result.add params[outputIndex].name + result.add ";\n" + result.add "}\n" + + of 3: + result.add "\nkernel void computeMain(uint3 gl_GlobalInvocationID [[thread_position_in_grid]]) {\n" + result.add bodyCode + result.add "}\n" + + else: + discard diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim new file mode 100644 index 0000000..fe16b1a --- /dev/null +++ b/src/shady/backends/shared.nim @@ -0,0 +1,1353 @@ +## Shader macro, converts Nim code into shader source. + +import macros, pixie, strutils, tables, vmath +import shady/backends/[glsl, glsl3, glsl4, dx12, metal4] +from chroma import ColorRGBX + +type + ShaderTarget* = enum + glsl3WebGL ## OpenGL ES 3.0 / WebGL 2.0 compatibility profile. + glsl3Desktop ## Desktop GLSL 3.30 / OpenGL 3.3 compatibility profile. + glsl4Desktop ## Explicit desktop GLSL for OpenGL 4.1+. + hlslDX12 ## HLSL for DirectX 12. + metalMSL ## Metal Shading Language. + + GlslTarget* = ShaderTarget + + ShaderStage* = enum + shaderAuto + shaderVertex + shaderFragment + shaderCompute + + ShaderLanguage = enum + langGlsl + langHlsl + langMetal + +const + glslDesktop* = glsl4Desktop + glslES3* = glsl3WebGL + +var useResult {.compiletime.}: bool +var shaderTarget* {.compiletime.}: ShaderTarget +var shaderStage* {.compiletime.}: ShaderStage + +template glslTarget*(): untyped = + shaderTarget + +proc err(msg: string, n: NimNode) {.noreturn.} = + error("[Shady] " & msg, n) + +proc language(target: ShaderTarget): ShaderLanguage = + case target + of glsl3WebGL, glsl3Desktop, glsl4Desktop: + langGlsl + of hlslDX12: + langHlsl + of metalMSL: + langMetal + +proc isGlsl(): bool = + shaderTarget.language == langGlsl + +proc isHlsl(): bool = + shaderTarget.language == langHlsl + +proc isMetal(): bool = + shaderTarget.language == langMetal + +proc typeRename(t: string): string = + ## Some shader type names don't match Nim names, rename here. + case shaderTarget.language + of langGlsl: + glslTypeRename(t) + of langHlsl: + hlslTypeRename(t) + of langMetal: + metalTypeRename(t) + +proc typeString(n: NimNode): string = + if n.kind != nnkBracketExpr: + typeRename(n.strVal) + else: + case n.repr: + of "GMat2[float32]": typeRename("Mat2") + of "GMat3[float32]": typeRename("Mat3") + of "GMat4[float32]": typeRename("Mat4") + of "GVec2[float32]": typeRename("Vec2") + of "GVec3[float32]": typeRename("Vec3") + of "GVec4[float32]": typeRename("Vec4") + of "GMat2[float64]": (if isGlsl(): "dmat2" else: "double2x2") + of "GMat3[float64]": (if isGlsl(): "dmat3" else: "double3x3") + of "GMat4[float64]": (if isGlsl(): "dmat4" else: "double4x4") + of "GVec2[float64]": typeRename("DVec2") + of "GVec3[float64]": typeRename("DVec3") + of "GVec4[float64]": typeRename("DVec4") + of "GVec2[uint32]": typeRename("UVec2") + of "GVec3[uint32]": typeRename("UVec3") + of "GVec4[uint32]": typeRename("UVec4") + of "GVec2[int32]": typeRename("IVec2") + of "GVec3[int32]": typeRename("IVec3") + of "GVec4[int32]": typeRename("IVec4") + of "Uniform[float32]": typeRename("float32") + of "Uniform[int]": typeRename("int") + else: + if n[0].repr == "array": + var length = 0 + if n[1].kind == nnkIntLit: + length = n[1].intVal.int + elif n[1].kind == nnkBracketExpr and n[1].len == 3 and n[1][0].repr == "..": + # array[0..1, T] case + length = n[1][2].intVal.int - n[1][1].intVal.int + 1 + elif n[1].kind == nnkInfix and n[1].len == 3 and n[1][0].repr == "..": + # array[0..1, T] case + length = n[1][2].intVal.int - n[1][1].intVal.int + 1 + + if length == 2: + if n[2].repr == "uint16": return typeRename("Vec2") + if n[2].repr in ["uint32"]: return typeRename("UVec2") + if n[2].repr in ["int16", "int32"]: return typeRename("IVec2") + if n[2].repr in ["float32", "float64"]: return typeRename("Vec2") + elif length == 3: + if n[2].repr == "uint16": return typeRename("Vec3") + if n[2].repr in ["uint32"]: return typeRename("UVec3") + if n[2].repr in ["int16", "int32"]: return typeRename("IVec3") + if n[2].repr in ["float32", "float64"]: return typeRename("Vec3") + elif length == 4: + if n[2].repr == "uint16": return typeRename("Vec4") + if n[2].repr in ["uint32"]: return typeRename("UVec4") + if n[2].repr in ["int16", "int32"]: return typeRename("IVec4") + if n[2].repr in ["float32", "float64"]: return typeRename("Vec4") + + err "can't figure out type: " & n.repr, n + +## Default constructor for different shader types. +proc typeDefault(t: string, n: NimNode): string = + result = + case shaderTarget.language + of langGlsl: + glslTypeDefault(t) + of langHlsl: + hlslTypeDefault(t) + of langMetal: + metalTypeDefault(t) + if result.len == 0: + err "no typeDefault " & t, n + +## Simply SKIP these functions. +const ignoreFunctions = [ + "echo", "print", "debugEcho", "$" +] + +proc isVectorAccess(s: string): bool = + ## is it a x,y,z or swizzle rgba= + for flavor in ["xyzw", "rgba", "stpq"]: + if s[0] in flavor: + for i, c in s: + if c notin flavor: + if c == '=' and i == s.len - 1: + return true + else: + return false + return true + return false + +proc procRename(t: string): string = + ## Some shader proc names don't match Nim names, rename here. + case shaderTarget.language + of langGlsl: + glslProcRename(t) + of langHlsl: + hlslProcRename(t) + of langMetal: + metalProcRename(t) + +proc opPrecedence(op: string): int = + ## Given an operator return its precedence. + ## Used to decide if () are needed. + ## See: https://learnwebgl.brown37.net/12_shader_language/glsl_mathematical_operations.html + case op: + of "*", "/": 4 + of "+", "-": 5 + of "<", ">", "<=", ">=": 7 + of "==", "!=": 8 + of "&&": 12 + of "^^": 13 + of "||": 14 + of "=", "+=", "-=", "*=", "/=": 16 + else: -1 + +proc getPrecedence(n: NimNode): int = + ## Return the opPrecedence of the node operator or -1. + if n.kind == nnkInfix: + n[0].strVal.opPrecedence() + else: + -1 + +proc addIndent(res: var string, level: int) = + ## Add indent (only if its needed). + if res.len == 0: + for i in 0 ..< level: + res.add " " + return + var + idx = res.len - 1 + spaces = 0 + while idx >= 0 and res[idx] == ' ': + dec idx + inc spaces + if idx < 0: + let level = max(0, level - spaces div 2) + for i in 0 ..< level: + res.add " " + return + if spaces == 0 and res[idx] != '\n': + res.add '\n' + let level = level - spaces div 2 + for i in 0 ..< level: + res.add " " + +proc addGap(res: var string) = + if res.len > 0: + if not res.endsWith("\n\n"): + if res.endsWith("\n"): + res.add "\n" + else: + res.add "\n\n" + +proc addSmart(res: var string, c: char, others = {'}'}) = + ## Ads a char but first checks if its already here. + var idx = res.len - 1 + while res[idx] in Whitespace: + dec idx + if res[idx] != c and res[idx] notin others: + res.add c + +proc toCode(n: NimNode, res: var string, level = 0) +proc toCodeStmts(n: NimNode, res: var string, level = 0) + +proc isIntegerType(t: string): bool = + t in ["int", "int8", "int16", "int32", "int64", "uint", "uint8", "uint16", "uint32", "uint64"] + +proc typeInstRepr(n: NimNode): string = + try: + n.getTypeInst().repr + except: + "" + +proc isSamplerBufferExpr(n: NimNode): bool = + "SamplerBuffer" in n.typeInstRepr() + +proc codeExpr(n: NimNode): string = + n.toCode(result) + +proc emitBackendCall(n: NimNode, res: var string): bool = + ## Builtin resource operations are where the shader languages differ most. + if isGlsl() or n.kind notin {nnkCall, nnkCommand}: + return false + + let name = n[0].strVal + if name notin ["texture", "texelFetch", "imageLoad", "imageStore"]: + return false + var args: seq[string] + for i in 1 ..< n.len: + args.add n[i].codeExpr() + + let call = + if isHlsl(): + hlslResourceCall(name, args, n.len > 1 and n[1].isSamplerBufferExpr) + else: + metalResourceCall(name, args, n.len > 1 and n[1].isSamplerBufferExpr) + if call.len == 0: + return false + res.add call + return true + +proc toCode(n: NimNode, res: var string, level = 0) = + ## Inner code block. + + case n.kind + + of nnkAsgn: + res.addIndent level + n[0].toCode(res) + res.add " = " + n[1].toCode(res) + res.addSmart ';' + + of nnkInfix: + if n[0].repr in ["mod"] and not isIntegerType(n[1].getType().repr): + # In Nim float mod and integer made are same thing. + # In GLSL mod(float, float) is a function while % is for integers. + if isGlsl(): + res.add "mod" + else: + res.add "fmod" + res.add "(" + n[1].toCode(res) + res.add ", " + n[2].toCode(res) + res.add ")" + + elif n[0].repr in ["+=", "-=", "*=", "/="]: + res.addIndent level + n[1].toCode(res) + res.add " " + n[0].toCode(res) + res.add " " + n[2].toCode(res) + res.addSmart ';' + + else: + let + a = n.getPrecedence() + l = n[1].getPrecedence() + r = n[2].getPrecedence() + if l >= a: + res.add "(" + n[1].toCode(res) + res.add ")" + else: + n[1].toCode(res) + res.add " " + n[0].toCode(res) + res.add " " + if r >= a: + res.add "(" + n[2].toCode(res) + res.add ")" + else: + n[2].toCode(res) + + of nnkHiddenDeref, nnkHiddenAddr: + n[0].toCode(res) + + of nnkCall, nnkCommand: + if n[0].strVal == "discardFragment": + # Special case for discarding the fragment. + # Because both Nim discard and GLSL discard are keywords that do different things. + if isMetal(): + res.add "discard_fragment()" + else: + res.add "discard" + return + if n[0].strVal == "inc": + n[1].toCode(res) + if n.len == 3: + res.add " += " + n[2].toCode(res) + else: + res.add "++" + return + if n[0].strVal == "dec": + n[1].toCode(res) + if n.len == 3: + res.add " -= " + n[2].toCode(res) + else: + res.add "--" + return + if n.emitBackendCall(res): + return + var procName = procRename(n[0].strVal) + if procName in ignoreFunctions: + return + if procName == "[]=": + n[1].toCode(res) + for i in 2 ..< n.len - 1: + res.add "[" + n[i].toCode(res) + res.add "]" + res.add " = " + n[n.len - 1].toCode(res) + res.addSmart ';' + + elif procName == "[]": + n[1].toCode(res) + for i in 2 ..< n.len: + res.add "[" + n[i].toCode(res) + res.add "]" + + elif isVectorAccess(procName): + if n[1].kind == nnkSym: + n[1].toCode(res) + else: + res.add "(" + n[1].toCode(res) + res.add ")" + res.add "." + res.add procName.replace("=", " = ") + if n.len == 3: + n[2].toCode(res) + else: + res.add procName + res.add "(" + for j in 1 ..< n.len: + if j != 1: res.add ", " + n[j].toCode(res) + res.add ")" + + of nnkDotExpr: + n[0].toCode(res) + res.add "." + n[1].toCode(res) + + of nnkBracketExpr: + if n[0].len == 2 and n[0][1].repr == "arr": + # Fastest vmath translates `obj.x` to `obj.arr[x]` for speed. + # Translate expanded the `obj.arr[x]` back to `.x` for shader. + let field = case n[1].repr: + of "0": ".x" + of "1": ".y" + of "2": ".z" + of "3": ".w" + else: "[" & n[1].repr & "]" + n[0][0].toCode(res) + res.add field + else: + n[0].toCode(res) + res.add "[" + n[1].toCode(res) + res.add "]" + + of nnkIdent, nnkSym: + res.add procRename(n.strVal) + + of nnkStmtListExpr: + for j in 0 ..< n.len: + n[j].toCode(res, level) + + of nnkStmtList: + for j in 0 ..< n.len: + if n[j].kind in [nnkCall, nnkCommand]: + res.addIndent level + n[j].toCode(res, level) + if n[j].kind notin [nnkLetSection, nnkVarSection, nnkCommentStmt]: + res.addSmart ';' + res.add "\n" + + of nnkIfStmt: + res.addIndent level + res.add "if (" + n[0][0].toCode(res) + res.add ") {\n" + n[0][1].toCodeStmts(res, level + 1) + res.addIndent level + res.add "}" + var i = 1 + while n.len > i: + if n[i].kind == nnkElse: + res.add " else {\n" + n[i][0].toCodeStmts(res, level + 1) + res.addIndent level + res.add "}" + elif n[i].kind == nnkElifBranch: + res.add " else if (" + n[i][0].toCode(res) + res.add ") {\n" + n[i][1].toCodeStmts(res, level + 1) + res.addIndent level + res.add "}" + else: + err "Not supported if branch", n + inc i + + # of nnkIfExpr: + # res.add "(" + # n[0][0].toCode(res) + # res.add ") ? (" + # n[1][0].toCode(res) + # res.add ") : (" + # n[2][0].toCode(res) + # res.add ")" + + of nnkConv: + res.add typeRename(n[0].strVal) + res.add "(" + n[1].toCode(res) + res.add ")" + + of nnkHiddenStdConv: + var typeStr = typeRename(n.getType.repr) + if typeStr.startsWith("range["): + n[1].toCode(res) + elif typeStr == "float" and n[1].kind == nnkIntLit: + res.add $n[1].intVal.float64 + elif typeStr == "float" and n[1].kind == nnkFloatLit: + res.add $n[1].floatVal.float64 + elif typeStr in ["int", "uint"] and n[1].kind in {nnkIntLit .. nnkInt64Lit}: + n[1].toCode(res) + else: + for j in 1 .. n.len-1: + res.add typeStr + res.add "(" + n[j].toCode(res) + res.add ")" + + of nnkNone: + assert false + + of nnkEmpty, nnkNilLit, nnkDiscardStmt, nnkPragma: + # Skip all nil, empty and discard statements. + discard + + of nnkIntLit .. nnkInt64Lit: + var iv = $n.intVal + res.add iv + + of nnkFloatLit .. nnkFloat64Lit: + var fv = $n.floatVal + res.add fv + + of nnkStrLit .. nnkTripleStrLit: + res.add $n.strVal.newLit.repr + + of nnkCommentStmt: + # preserve comments + for line in n.strVal.split("\n"): + res.addIndent level + res.add "// " + res.add line + res.add "\n" + + of nnkVarSection, nnkLetSection: + ## var and let ares the same in GLSL + for j in 0 ..< n.len: + res.addIndent level + n[j].toCode(res, level) + res.addSmart ';' + res.add "\n" + + of nnkIdentDefs: + for j in countup(0, n.len - 1, 3): + var typeStr = "" + if n[1].kind == nnkBracketExpr and + n[1][0].kind == nnkSym and + n[1][0].strVal == "array": + typeStr = typeRename(n[1][2].strVal) + typeStr.add "[" + typeStr.add n[1][1].repr + typeStr.add "]" + + res.add typeStr + res.add " " + n[0].toCode(res) + else: + typeStr = typeString(n[j].getTypeInst()) + res.add typeStr + res.add " " + n[j].toCode(res) + if n[j + 2].kind != nnkEmpty: + res.add " = " + n[j + 2].toCode(res) + else: + res.add " = " + res.add typeDefault(typeStr, n[j]) + + of nnkReturnStmt: + res.addIndent level + if n[0].kind == nnkAsgn: + n[0].toCode(res) + res.add "\n" + res.addIndent level + res.add "return result" + elif n[0].kind != nnkEmpty: + res.add "return " + n[0][1].toCode(res) + elif useResult: + res.add "return result" + else: + res.add "return" + + of nnkPrefix: + res.add procRename(n[0].strVal) & " (" + n[1].toCode(res) + res.add ")" + + of nnkWhileStmt: + res.addIndent level + res.add "while(" + n[0].toCode(res) + res.add ") {\n" + n[1].toCode(res, level + 1) + res.addIndent level + res.add "}" + + of nnkForStmt: + res.addIndent level + res.add "for(" + res.add "int " + res.add n[0].strVal + res.add " = " + n[1][1].toCode(res) + res.add "; " + res.add n[0].strVal + if n[1][0].strVal == "..<": + res.add " < " + elif n[1][0].strVal == "..": + res.add " <= " + else: + err "For loop only supports integer .. or ..<.", n + n[1][2].toCode(res) + res.add "; " + res.add n[0].strVal + res.add "++" + res.add ") {\n" + if n[2].kind == nnkStmtList: + n[2].toCode(res, level + 1) + else: + res.addIndent level + n[2].toCode(res, level + 1) + res.add ";" + res.addIndent level + res.add "}" + + of nnkBreakStmt: + res.addIndent level + res.add "break" + + of nnkProcDef: + err "Nested proc definitions are not allowed.", n + + of nnkCaseStmt: + res.addIndent level + res.add "switch(" + n[0].toCode(res) + res.add ") {\n" + for branch in n[1 .. ^1]: + if branch.kind == nnkOfBranch: + res.addIndent level + res.add "case " + branch[0].toCode(res) + res.add ":{\n" + branch[1].toCodeStmts(res, level + 1) + res.addIndent level + if branch[1].kind == nnkReturnStmt or branch[1].kind == nnkBreakStmt: + res.add "};\n" + else: + res.add "}; break;\n" + elif branch.kind == nnkElse: + res.addIndent level + res.add "default: {\n" + branch[0].toCodeStmts(res, level + 1) + res.addIndent level + if branch[0].kind == nnkReturnStmt or branch[0].kind == nnkBreakStmt: + res.add "};\n" + else: + res.add "}; break;\n" + else: + err "Can't compile branch", n + res.addIndent level + res.add "}" + + of nnkChckRange: + # skip check range and treat it as a hidden cast instead + var typeStr = typeRename(n.getType.repr) + res.add typeStr + res.add "(" + n[0].toCode(res) + res.add ")" + + of nnkObjConstr: + if repr(n[0][0]) == "[]": + # probably a swizzle call. + res.add n[1][1][1][1][0][0].strval + res.add "." + for part in n[1][1]: + res.add "xyzw"[part[1][1][1].intVal] + else: + echo n.treeRepr + err "Some sort of object constructor", n + + of nnkIfExpr: + var gotElse = false + res.add "(" + for subn in n: + case subn.kind + of nnkElifExpr: + if gotElse: + echo n.treeRepr + err "Cannot have elif after else", n + res.add "(" + subn[0].toCode(res) + res.add ") ? (" + subn[1].toCode(res) + res.add ") : " + of nnkElseExpr: + gotElse = true + res.add "(" + subn[0].toCode(res) + res.add ")" + else: + echo n.treeRepr + err "Invalid child of nnkIfExpr: " & subn.kind.repr, n + res.add ")" + if not gotElse: + echo n.treeRepr + err "nnkIfExpr is missing an else clause", n + + else: + echo n.treeRepr + err "Can't compile", n + +proc toCodeStmts(n: NimNode, res: var string, level = 0) = + if n.kind != nnkStmtList: + res.addIndent level + n.toCode(res, level) + res.addSmart ';' + res.add "\n" + else: + n.toCode(res, level) + +type EntryParam = tuple[name, typ: string, isOut: bool] + +proc typeFromNode(typeNode: NimNode): string = + if typeNode.kind == nnkBracketExpr: + typeString(typeNode) + else: + typeRename(typeNode.strVal) + +proc innerTypeFromVar(typeNode: NimNode): string = + if typeNode[0].kind == nnkBracketExpr: + typeString(typeNode[0]) + else: + typeRename(typeNode[0].strVal) + +proc gatherEntryParams(formalParams: NimNode): seq[EntryParam] = + for paramDefs in formalParams: + if paramDefs.kind == nnkIdentDefs: + let typeNode = paramDefs[^2] + for i in 0 ..< paramDefs.len - 2: + let param = paramDefs[i] + if typeNode.kind == nnkVarTy: + result.add ( + name: param.strVal, + typ: innerTypeFromVar(typeNode), + isOut: true + ) + else: + result.add ( + name: param.strVal, + typ: typeFromNode(typeNode), + isOut: false + ) + +proc resolvedStage(params: seq[EntryParam]): ShaderStage = + if shaderStage != shaderAuto: + return shaderStage + for p in params: + if p.name == "gl_Position": + return shaderVertex + shaderFragment + +proc stageId(stage: ShaderStage): int = + case stage + of shaderVertex: 1 + of shaderFragment: 2 + of shaderCompute: 3 + of shaderAuto: 0 + +proc emitBackendEntry(params: seq[EntryParam], body: NimNode, res: var string) = + var bodyCode = "" + body.toCodeStmts(bodyCode, 1) + let stage = resolvedStage(params).stageId() + if isHlsl(): + res.add dx12.emitHlslEntry(params, bodyCode, stage) + else: + res.add metal4.emitMetalEntry(params, bodyCode, stage) + +proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = + ## Top level block such as in and out params. + ## Generates the main function (which is not like all the other functions) + + assert topLevelNode.kind == nnkProcDef + + if not isGlsl(): + var formalParams = newEmptyNode() + var body = newEmptyNode() + for n in topLevelNode: + case n.kind + of nnkFormalParams: + formalParams = n + of nnkEmpty, nnkSym, nnkPragma: + discard + else: + body = n + let params = gatherEntryParams(formalParams) + emitBackendEntry(params, body, res) + return + + for n in topLevelNode: + case n.kind + of nnkEmpty: + discard + of nnkSym: + discard + of nnkFormalParams: + ## Main function parameters are different in they they go in as globals. + res.addGap() + for paramDefs in n: + if paramDefs.kind == nnkIdentDefs: + let typeNode = paramDefs[^2] + for i in 0 ..< paramDefs.len - 2: + let param = paramDefs[i] + if param.strVal in ["gl_FragColor", "gl_Position"]: + continue + if typeNode.kind == nnkVarTy: + if typeNode[0].repr == "seq": + res.add "buffer?" + res.add typeNode.repr + continue + elif typeNode[0].repr == "int": + res.add "flat " + res.add "out " + res.add typeRename(typeNode[0].strVal) + else: + if typeNode.kind == nnkBracketExpr: + res.add "in " + res.add typeString(typeNode) + else: + if shaderTarget != glslES3 and param.strVal == "gl_FragCoord": + res.add "layout(origin_upper_left) " + if typeNode.strVal == "int": + res.add "flat " + res.add "in " + res.add typeRename(typeNode.strVal) + res.add " " + res.add param.strVal + res.addSmart ';' + res.add "\n" + else: + res.addGap() + res.add "void main() {\n" + n.toCodeStmts(res, level+1) + res.add "}\n" + +proc hasResult(node: NimNode): bool = + if node.kind == nnkSym and node.strVal == "result": + return true + for c in node.children: + if c.hasResult(): + return true + return false + +proc procDef(topLevelNode: NimNode): string = + ## Process whole function (that is not the main function). + + var procName = "" + var paramsStr = "" + var returnType = "void" + + assert topLevelNode.kind in {nnkFuncDef, nnkProcDef} + for n in topLevelNode: + case n.kind + of nnkEmpty, nnkPragma: + discard + of nnkSym: + procName = $n + of nnkFormalParams: + # Reading parameter list `(x, y, z: float)` + if n[0].kind != nnkEmpty: + returnType = typeRename(n[0].strVal) + for paramDef in n[1 .. ^1]: + # The paramDef is like `x, y, z: float`. + if paramDef.kind != nnkEmpty: + for param in paramDef[0 ..< ^2]: + # Process each `x`, `y`, `z` in a loop. + paramsStr.add " " + let paramName = param.repr() + let paramType = param.getTypeInst() + if paramType.kind == nnkVarTy: + # Process `x: var float` + if isGlsl() and paramType[0].strVal == "int": + paramsStr.add "flat " + if isMetal(): + paramsStr.add "thread " + paramsStr.add typeRename(paramType[0].strVal) + paramsStr.add "&" + else: + paramsStr.add "inout " + paramsStr.add typeRename(paramType[0].strVal) + elif paramType.kind == nnkBracketExpr: + # Process varying[uniform]. + # TODO test? + paramsStr.add paramType[0].strVal + paramsStr.add " " + paramsStr.add typeRename(paramType[1].strVal) + else: + # Just a simple `x: float` case. + if isGlsl() and paramType.strVal == "int": + paramsStr.add "flat " + paramsStr.add typeRename(paramType.strVal) + paramsStr.add " " + paramsStr.add paramName + paramsStr.add ",\n" + else: + if paramsStr.len > 0: + paramsStr = paramsStr[0 .. ^3] & "\n" + result.add returnType & " " & procName & "(\n" & paramsStr & ") {\n" + useResult = n.hasResult() + if useResult: + result.addIndent(1) + result.add returnType + result.add " result;" + n.toCodeStmts(result, 1) + if useResult: + if "return result" notin result[^20..^1]: + result.addIndent(1) + result.add "return result;\n" + result.add "}" + +proc gatherTypes( + typeInst: NimNode, + types: var Table[string, string] +) = + ## Looks for types used and gathers their struct definitions. + var typeInst = typeInst + if typeInst.kind == nnkVarTy: + typeInst = typeInst[0] + if typeInst.kind == nnkBracketExpr: + for i in 1 ..< typeInst.len: + gatherTypes(typeInst[i], types) + return + + if typeInst.kind == nnkSym: + let name = typeInst.strVal + if name in types: return + if typeRename(name) != name: return + if name in ["ColorRGBX"]: + # Special case for ColorRGBX which is handled as vec4. + return + + let impl = typeInst.getImpl() + if impl.kind == nnkTypeDef and impl[2].kind == nnkObjectTy: + var def = "struct " & name & " {\n" + let obj = impl[2] + let reclist = obj[2] + for field in reclist: + if field.kind == nnkIdentDefs: + let fieldTypeNode = field[^2] + let fieldType = fieldTypeNode.getTypeInst() + gatherTypes(fieldType, types) + for i in 0 ..< field.len - 2: + var fieldName = field[i].repr + if fieldName.endsWith("*"): + fieldName = fieldName[0 .. ^2] + def.add " " & typeString(fieldType) & " " & fieldName & ";\n" + def.add "};" + types[name] = def + +proc gatherFunction( + topLevelNode: NimNode, + functions: var Table[string, string], + globals: var Table[string, string], + types: var Table[string, string] +) = + + ## Looks for functions this function calls and brings them up + for n in topLevelNode: + if n.kind == nnkSym: + # Looking for globals. + let name = n.strVal + if name notin glslGlobals and name notin glslFunctions and name notin globals: + if n.owner().symKind == nskModule: + let impl = n.getImpl() + if impl.kind notin { + nnkTypeDef, + nnkIteratorDef, + nnkProcDef, + nnkFuncDef + } and impl.kind != nnkNilLit: + var defStr = "" + let typeInst = n.getTypeInst + gatherTypes(typeInst, types) + if typeInst.kind == nnkBracketExpr: + # might be a uniform + if typeInst[0].repr in ["Uniform", "UniformWriteOnly", "Attribute"]: + let samplerType = typeString(typeInst[1]) + if isGlsl(): + defStr.add typeRename(typeInst[0].repr) + defStr.add " " + if shaderTarget == glslES3 and glsl3NeedsHighp(samplerType): + defStr.add "highp " + defStr.add samplerType + elif isHlsl(): + defStr.add samplerType + else: + if typeInst[0].repr == "Uniform": + defStr.add "constant " + defStr.add samplerType + elif typeInst[0].repr == "UniformWriteOnly": + defStr.add samplerType + else: + defStr.add samplerType + elif typeInst[0].repr == "array": + defStr.add typeString(typeInst[2]) + defStr.add "[" + defStr.add typeRename(typeInst[1][2].repr) + defStr.add "]" + else: + err "Invalid x[y].", n + else: + defStr.add typeRename(typeInst.repr) + defStr.add " " & name + if impl[2].kind != nnkEmpty: + defStr.add " = " & repr(impl[2]) + defStr.addSmart ';' + if defStr notin ["uniform Uniform = T;", + "attribute Attribute = T;"]: + globals[name] = defStr + if isHlsl() and typeInst.kind == nnkBracketExpr and + typeInst[0].repr == "Uniform" and + typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]: + globals[name & "Sampler"] = hlslSamplerDecl(name) + + if n.kind == nnkCall: + # Looking for functions. + if repr(n[0]) == "[]": + continue + let procName = repr n[0] + if procName in ignoreFunctions: + continue + if procName notin glslFunctions and + procName notin functions and + not isVectorAccess(procName): + ## If its not a builtin proc, we need to bring definition. + let impl = n[0].getImpl() + gatherFunction(impl, functions, globals, types) + functions[procName] = procDef(impl) + + if n.kind == nnkFormalParams: + for i in 1 ..< n.len: + let paramDef = n[i] + let paramType = paramDef[^2].getTypeInst() + gatherTypes(paramType, types) + + gatherFunction(n, functions, globals, types) + +proc toGLSLInner*(s: NimNode, version, extra: string): string = + + var code: string + + # Add shader header stuff. + if isGlsl(): + if version.len > 0: + code.add "#version " & version & "\n" + code.add extra + elif isMetal(): + code.add metalHeader + elif isHlsl(): + code.add hlslHeader + code.add "// from " & s.strVal & "\n" + + var n = getImpl(s) + + # Gather all globals and functions, and globals and functions they use. + var functions: Table[string, string] + var globals: Table[string, string] + var types: Table[string, string] + gatherFunction(n, functions, globals, types) + + # Put types first. + code.addGap() + for k, v in types: + code.add(v) + code.add "\n" + + # Put globals next. + code.addGap() + for k, v in globals: + code.add(v) + code.add "\n" + + # Put functions definition (just name and types part). + code.addGap() + for k, v in functions: + var funCode = v.split(" {")[0] + funCode = funCode + .replace("\n", "") + .replace(" ", " ") + .replace(", ", ", ") + .replace("( ", "(") + code.add funCode + code.addSmart ';' + code.add "\n" + + # Put functions (with bodies) next. + code.addGap() + for k, v in functions: + code.add v + code.add "\n" + + # Put the main function last. + toCodeTopLevel(n, code) + + return code + +proc toGLSLInner*(s: NimNode, target: GlslTarget): string = + ## Converts proc to a GLSL string for the given target. + shaderTarget = target + shaderStage = shaderAuto + case target + of glsl4Desktop: + toGLSLInner(s, glsl4DesktopVersion, glsl4DesktopExtra) + of glsl3Desktop: + toGLSLInner(s, glsl3DesktopVersion, glsl3DesktopExtra) + of glsl3WebGL: + toGLSLInner(s, glsl3WebGLVersion, glsl3WebGLExtra) + of hlslDX12: + toGLSLInner(s, "", "") + of metalMSL: + toGLSLInner(s, "", "") + +proc toShaderInner*( + s: NimNode, + target: ShaderTarget, + stage = shaderAuto +): string = + ## Converts proc to shader source for the given target. + shaderTarget = target + shaderStage = stage + case target + of glsl4Desktop: + toGLSLInner(s, glsl4DesktopVersion, glsl4DesktopExtra) + of glsl3Desktop: + toGLSLInner(s, glsl3DesktopVersion, glsl3DesktopExtra) + of glsl3WebGL: + toGLSLInner(s, glsl3WebGLVersion, glsl3WebGLExtra) + of hlslDX12, metalMSL: + toGLSLInner(s, "", "") + +macro toGLSL*( + s: typed, + target: static[GlslTarget] +): string = + ## Converts proc to a GLSL string for the given target. + newLit(toGLSLInner(s, target)) + +macro toGLSL*( + s: typed, + version = "410", + extra = "precision highp float;\n" +): string = + ## Converts proc to a glsl string. + ## For target-aware compilation, use the GlslTarget overload instead. + shaderTarget = if "es" in version.strVal: glslES3 else: glslDesktop + shaderStage = shaderAuto + newLit(toGLSLInner(s, version.strVal, extra.strVal)) + +macro toShader*( + s: typed, + target: static[ShaderTarget], + stage: static[ShaderStage] = shaderAuto +): string = + ## Converts proc to shader source for the given target and stage. + newLit(toShaderInner(s, target, stage)) + +macro toHLSL*( + s: typed, + stage: static[ShaderStage] = shaderAuto +): string = + ## Converts proc to HLSL source for DirectX 12. + newLit(toShaderInner(s, hlslDX12, stage)) + +macro toMSL*( + s: typed, + stage: static[ShaderStage] = shaderAuto +): string = + ## Converts proc to Metal Shading Language source. + newLit(toShaderInner(s, metalMSL, stage)) + +## GLSL helper functions + +type + Uniform*[T] = T + UniformWriteOnly*[T] = T + Attribute*[T] = T + + SamplerBuffer* = object + data*: seq[float32] + + ImageBuffer* = object + image*: Image + + UImageBuffer* = object + image*: Image + + Sampler2d* = object + image*: Image + + USampler2d* = object + image*: Image + + Sampler2dArray* = object + images*: seq[Image] + +var + ## GLSL globals. + gl_Position*: Vec4 + gl_VertexID*: int32 + +proc texelFetch*(buffer: Uniform[SamplerBuffer], index: SomeInteger): Vec4 = + vec4(buffer.data[index.int], 0, 0, 0) + +proc texelFetch*(buffer: Uniform[Sampler2D], pos: IVec2, level: int): Vec4 = + let c = buffer.image[pos.x.int, pos.y.int] + return vec4(c.r.float32/255, c.g.float32/255, c.b.float32/255, c.a.float32/255) + +proc texelFetch*(buffer: Uniform[USampler2D], pos: IVec2, level: int): UVec4 = + ## CPU stub for usampler2D; not used at runtime. Returns zeros. + uvec4(0u32, 0u32, 0u32, 0u32) + +proc imageLoad*( + buffer: var UniformWriteOnly[UImageBuffer], index: int32 +): UVec4 = + result.x = buffer.image.data[index.int].r + result.g = buffer.image.data[index.int].g + result.b = buffer.image.data[index.int].b + result.a = buffer.image.data[index.int].a + +proc imageStore*(buffer: var UniformWriteOnly[UImageBuffer], index: int32, + color: UVec4) = + buffer.image.data[index.int].r = clamp(color.x, 0, 255).uint8 + buffer.image.data[index.int].g = clamp(color.y, 0, 255).uint8 + buffer.image.data[index.int].b = clamp(color.z, 0, 255).uint8 + buffer.image.data[index.int].a = clamp(color.w, 0, 255).uint8 + +proc imageStore*(buffer: var UniformWriteOnly[ImageBuffer], index: int32, + color: Vec4) = + buffer.image.data[index.int].r = clamp(color.x*255, 0, 255).uint8 + buffer.image.data[index.int].g = clamp(color.y*255, 0, 255).uint8 + buffer.image.data[index.int].b = clamp(color.z*255, 0, 255).uint8 + buffer.image.data[index.int].a = clamp(color.w*255, 0, 255).uint8 + +proc imageStore*(buffer: var Uniform[Sampler2D], pos: IVec2, + color: Vec4) = + buffer.image[pos.x.int, pos.y.int] = rgbx( + clamp(color.x*255, 0, 255).uint8, + clamp(color.y*255, 0, 255).uint8, + clamp(color.z*255, 0, 255).uint8, + clamp(color.w*255, 0, 255).uint8, + ) + +proc vec4*(c: ColorRGBX): Vec4 = + vec4( + c.r.float32/255, + c.g.float32/255, + c.b.float32/255, + c.a.float32/255 + ) + +proc dFdx*(a: float32): float32 = + raise newException(Exception, "dFdx is not implemented") + +proc dFdx*(a: Vec2): Vec2 = + raise newException(Exception, "dFdx is not implemented") + +proc dFdx*(a: Vec3): Vec3 = + raise newException(Exception, "dFdx is not implemented") + +proc dFdx*(a: Vec4): Vec4 = + raise newException(Exception, "dFdx is not implemented") + +proc dFdy*(a: float32): float32 = + raise newException(Exception, "dFdy is not implemented") + +proc dFdy*(a: Vec2): Vec2 = + raise newException(Exception, "dFdy is not implemented") + +proc dFdy*(a: Vec3): Vec3 = + raise newException(Exception, "dFdy is not implemented") + +proc dFdy*(a: Vec4): Vec4 = + raise newException(Exception, "dFdy is not implemented") + +proc fwidth*(a: float32): float32 = + raise newException(Exception, "fwidth is not implemented") + +proc fwidth*(a: Vec2): Vec2 = + raise newException(Exception, "fwidth is not implemented") + +proc fwidth*(a: Vec3): Vec3 = + raise newException(Exception, "fwidth is not implemented") + +proc fwidth*(a: Vec4): Vec4 = + raise newException(Exception, "fwidth is not implemented") + +proc fmod*(x, y: float32): float32 = + x - y * floor(x / y) + +proc fmod*(x: Vec2, y: float32): Vec2 = + vec2(fmod(x.x, y), fmod(x.y, y)) + +proc fmod*(x: Vec3, y: float32): Vec3 = + vec3(fmod(x.x, y), fmod(x.y, y), fmod(x.z, y)) + +proc fmod*(x: Vec4, y: float32): Vec4 = + vec4(fmod(x.x, y), fmod(x.y, y), fmod(x.z, y), fmod(x.w, y)) + +proc fmod*(x, y: Vec2): Vec2 = + vec2(fmod(x.x, y.x), fmod(x.y, y.y)) + +proc fmod*(x, y: Vec3): Vec3 = + vec3(fmod(x.x, y.x), fmod(x.y, y.y), fmod(x.z, y.z)) + +proc fmod*(x, y: Vec4): Vec4 = + vec4(fmod(x.x, y.x), fmod(x.y, y.y), fmod(x.z, y.z), fmod(x.w, y.w)) + +proc fract*(a: float32): float32 = + a - floor(a) + +proc fract*(a: Vec2): Vec2 = + vec2(fract(a.x), fract(a.y)) + +proc fract*(a: Vec3): Vec3 = + vec3(fract(a.x), fract(a.y), fract(a.z)) + +proc fract*(a: Vec4): Vec4 = + vec4(fract(a.x), fract(a.y), fract(a.z), fract(a.w)) + +proc smoothstep*(a, b, x: float32): float32 = + let t = clamp((x - a) / (b - a), 0.0, 1.0) + t * t * (3.0 - 2.0 * t) + +proc smoothstep*(a, b, x: Vec2): Vec2 = + vec2(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y)) + +proc smoothstep*(a, b, x: Vec3): Vec3 = + vec3(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y), smoothstep(a.z, b.z, x.z)) + +proc smoothstep*(a, b, x: Vec4): Vec4 = + vec4(smoothstep(a.x, b.x, x.x), smoothstep(a.y, b.y, x.y), smoothstep(a.z, b.z, x.z), smoothstep(a.w, b.w, x.w)) + +proc texture*(buffer: Uniform[Sampler2D], pos: Vec2): Vec4 = + let pos = pos - vec2(0.5 / buffer.image.width.float32, 0.5 / + buffer.image.height.float32) + buffer.image.getRgbaSmooth( + ((pos.x mod 1.0) * buffer.image.width.float32), + ((pos.y mod 1.0) * buffer.image.height.float32) + ).vec4() + +proc textureSize*(buffer: Uniform[Sampler2D], level: int): Vec2 = + vec2(buffer.image.width.float32, buffer.image.height.float32) + +proc textureGrad*( + s: Uniform[Sampler2D], + uv: Vec3, + dUVdx: Vec2, + dUVdy: Vec2 +): Vec4 = + texture(s, uv.xy) + +proc textureGrad*( + s: Uniform[Sampler2DArray], + uvw: Vec3, + dUVdx: Vec2, + dUVdy: Vec2 +): Vec4 = + vec4(0, 0, 0, 0) + +proc discardFragment*() = + discard diff --git a/tests/test_backends.nim b/tests/test_backends.nim new file mode 100644 index 0000000..a688aca --- /dev/null +++ b/tests/test_backends.nim @@ -0,0 +1,157 @@ +import shady, strutils, vmath + +block: + proc fragmentConstant(fragColor: var Vec4) = + fragColor = vec4(1.0, 0.25, 0.0, 1.0) + + let glsl3Web = toShader(fragmentConstant, glsl3WebGL, shaderFragment) + doAssert "#version 300 es" in glsl3Web + doAssert "precision highp float;" in glsl3Web + doAssert "out vec4 fragColor;" in glsl3Web + + let glsl3DesktopSource = toShader(fragmentConstant, glsl3Desktop, shaderFragment) + doAssert "#version 330" in glsl3DesktopSource + doAssert "out vec4 fragColor;" in glsl3DesktopSource + + let glsl4 = toShader(fragmentConstant, glsl4Desktop, shaderFragment) + doAssert "#version 410" in glsl4 + doAssert "out vec4 fragColor;" in glsl4 + + let hlsl = toHLSL(fragmentConstant, shaderFragment) + doAssert "float4 PSMain()" in hlsl + doAssert ": SV_TARGET" in hlsl + doAssert "float4 fragColor = float4(0.0, 0.0, 0.0, 0.0);" in hlsl + doAssert "return fragColor;" in hlsl + + let msl = toMSL(fragmentConstant, shaderFragment) + doAssert "#include " in msl + doAssert "fragment float4 fragmentMain()" in msl + doAssert "float4 fragColor = float4(0.0, 0.0, 0.0, 0.0);" in msl + doAssert "return fragColor;" in msl + +block: + proc vertexPass( + vPos: Vec3, + vCol: Vec3, + gl_Position: var Vec4, + vertColor: var Vec3 + ) = + gl_Position = vec4(vPos.x, vPos.y, vPos.z, 1.0) + vertColor = vCol + + let hlsl = toHLSL(vertexPass, shaderVertex) + doAssert "struct VSOutput" in hlsl + doAssert "VSOutput VSMain(" in hlsl + doAssert "float4 pos : SV_POSITION;" in hlsl + doAssert "output.pos = gl_Position;" in hlsl + + let msl = toMSL(vertexPass, shaderVertex) + doAssert "struct VertexOut" in msl + doAssert "vertex VertexOut vertexMain(" in msl + doAssert "float4 position [[position]];" in msl + doAssert "output.position = gl_Position;" in msl + +block: + var atlas: Uniform[Sampler2d] + + proc sampledTexture(uv: Vec2, fragColor: var Vec4) = + fragColor = texture(atlas, uv) + + let hlsl = toHLSL(sampledTexture, shaderFragment) + doAssert "Texture2D atlas;" in hlsl + doAssert "SamplerState atlasSampler;" in hlsl + doAssert "atlas.Sample(atlasSampler, uv)" in hlsl + + let msl = toMSL(sampledTexture, shaderFragment) + doAssert "texture2d atlas;" in msl + doAssert "atlas.sample(atlasSampler, uv)" in msl + +block: + var atlas: Uniform[Sampler2d] + + func controlTint(texColor, vertexColor: Vec4): Vec4 = + result = vec4( + texColor.x * vertexColor.x, + texColor.y * vertexColor.y, + texColor.z * vertexColor.z, + texColor.w * vertexColor.w + ) + for i in 0 ..< 2: + result.x += 0.05 + var steps = 0 + while steps < 2: + result.z += 0.125 + inc steps + if result.w < 0.5: + result = vec4(0.0, 0.0, 0.0, 1.0) + + proc layoutVertex( + position: Vec3, + uv: Vec2, + vertexColor: Vec4, + gl_Position: var Vec4, + fragmentUv: var Vec2, + fragmentColor: var Vec4 + ) = + gl_Position = vec4(position.x, position.y, position.z, 1.0) + fragmentUv = uv + fragmentColor = vertexColor + + proc layoutFragment( + fragmentUv: Vec2, + fragmentColor: Vec4, + fragColor: var Vec4 + ) = + let texColor = texture(atlas, fragmentUv) + fragColor = controlTint(texColor, fragmentColor) + + let glsl3Vertex = toShader(layoutVertex, glsl3Desktop, shaderVertex) + doAssert "in vec3 position;" in glsl3Vertex + doAssert "in vec2 uv;" in glsl3Vertex + doAssert "in vec4 vertexColor;" in glsl3Vertex + doAssert "out vec2 fragmentUv;" in glsl3Vertex + doAssert "out vec4 fragmentColor;" in glsl3Vertex + + let glsl3Fragment = toShader(layoutFragment, glsl3Desktop, shaderFragment) + doAssert "uniform sampler2D atlas;" in glsl3Fragment + doAssert "vec4 controlTint" in glsl3Fragment + doAssert "for(int i = 0; i < 2; i++)" in glsl3Fragment + doAssert "while(steps < 2)" in glsl3Fragment + doAssert "if (" in glsl3Fragment + doAssert "texture(atlas, fragmentUv)" in glsl3Fragment + + let glsl4Vertex = toShader(layoutVertex, glsl4Desktop, shaderVertex) + doAssert "#version 410" in glsl4Vertex + doAssert "in vec3 position;" in glsl4Vertex + + let hlslVertex = toHLSL(layoutVertex, shaderVertex) + doAssert "float3 position : POSITION0" in hlslVertex + doAssert "float2 uv : TEXCOORD0" in hlslVertex + doAssert "float4 vertexColor : COLOR0" in hlslVertex + doAssert "float2 fragmentUv : TEXCOORD0" in hlslVertex + doAssert "float4 fragmentColor : COLOR0" in hlslVertex + + let hlslFragment = toHLSL(layoutFragment, shaderFragment) + doAssert "Texture2D atlas;" in hlslFragment + doAssert "atlas.Sample(atlasSampler, fragmentUv)" in hlslFragment + doAssert "float4 controlTint" in hlslFragment + doAssert "for(int i = 0; i < 2; i++)" in hlslFragment + doAssert "while(steps < 2)" in hlslFragment + doAssert "if (" in hlslFragment + + let mslVertex = toMSL(layoutVertex, shaderVertex) + doAssert "float3 position [[attribute(0)]]" in mslVertex + doAssert "float2 uv [[attribute(1)]]" in mslVertex + doAssert "float4 vertexColor [[attribute(2)]]" in mslVertex + doAssert "float2 fragmentUv;" in mslVertex + doAssert "float4 fragmentColor;" in mslVertex + + let mslFragment = toMSL(layoutFragment, shaderFragment) + doAssert "texture2d atlas;" in mslFragment + doAssert "atlas.sample(atlasSampler, fragmentUv)" in mslFragment + doAssert "float4 controlTint" in mslFragment + doAssert "for(int i = 0; i < 2; i++)" in mslFragment + doAssert "while(steps < 2)" in mslFragment + doAssert "if (" in mslFragment + +echo "Backend codegen tests passed" diff --git a/tests/test_cpu.nim b/tests/test_cpu.nim new file mode 100644 index 0000000..ae0fc6b --- /dev/null +++ b/tests/test_cpu.nim @@ -0,0 +1,77 @@ +import pixie, shady, vmath + +block: + proc cpuFragment(uv: Vec2, time: Uniform[float32], fragColor: var Vec4) = + let pulse = 0.5'f32 + 0.5'f32 * sin(time) + fragColor = vec4(uv.x, uv.y, pulse, 1.0) + + var color: Vec4 + cpuFragment(vec2(0.25, 0.75), 0.0'f32, color) + doAssert abs(color.x - 0.25) < 0.0001 + doAssert abs(color.y - 0.75) < 0.0001 + doAssert abs(color.z - 0.5) < 0.0001 + doAssert color.w == 1.0 + +block: + var atlas: Uniform[Sampler2d] + atlas.image = newImage(4, 4) + atlas.image.fill(color(0.2, 0.4, 0.8, 1.0)) + + proc cpuTexture(fragColor: var Vec4) = + fragColor = texture(atlas, vec2(0.5, 0.5)) + + var color: Vec4 + cpuTexture(color) + doAssert abs(color.x - 0.2) < 0.01 + doAssert abs(color.y - 0.4) < 0.01 + doAssert abs(color.z - 0.8) < 0.01 + doAssert abs(color.w - 1.0) < 0.01 + +block: + type CpuVertex = object + position: Vec3 + uv: Vec2 + color: Vec4 + + var atlas: Uniform[Sampler2d] + atlas.image = newImage(2, 2) + atlas.image[0, 0] = rgbx(255, 0, 0, 255) + atlas.image[1, 0] = rgbx(0, 255, 0, 255) + atlas.image[0, 1] = rgbx(0, 0, 255, 255) + atlas.image[1, 1] = rgbx(255, 255, 255, 255) + + proc controlTint(texel, tint: Vec4): Vec4 = + result = texel + for i in 0 ..< 2: + result.x += 0.05 + var steps = 0 + while steps < 2: + result.z += 0.125 + inc steps + if tint.y > 0.4: + result.y = result.y * tint.y + else: + result.y = 0.0 + result.w = tint.w + + proc cpuImageLayout(vertex: CpuVertex, fragColor: var Vec4) = + var texel = texelFetch(atlas, ivec2(1, 0), 0) + if vertex.position.x < 0.0: + texel = texelFetch(atlas, ivec2(0, 0), 0) + fragColor = controlTint(texel, vertex.color) + + var color: Vec4 + cpuImageLayout( + CpuVertex( + position: vec3(0.25, -0.25, 0.0), + uv: vec2(0.75, 0.25), + color: vec4(1.0, 0.5, 0.75, 1.0) + ), + color + ) + doAssert abs(color.x - 0.1) < 0.0001 + doAssert abs(color.y - 0.5) < 0.0001 + doAssert abs(color.z - 0.25) < 0.0001 + doAssert abs(color.w - 1.0) < 0.0001 + +echo "CPU execution tests passed" diff --git a/tests/test_directx.nim b/tests/test_directx.nim new file mode 100644 index 0000000..a65c814 --- /dev/null +++ b/tests/test_directx.nim @@ -0,0 +1,380 @@ +when defined(windows) and defined(shadyRunDx12): + import shady, vmath + import windy + import dx12, dx12/context, dx12/extras + + type DxVertex = object + position: array[3, float32] + uv: array[2, float32] + color: array[4, float32] + + const DxVertices = [ + DxVertex( + position: [-0.8'f32, -0.8'f32, 0.0'f32], + uv: [0.0'f32, 0.0'f32], + color: [1.0'f32, 0.25'f32, 0.0'f32, 1.0'f32] + ), + DxVertex( + position: [0.8'f32, -0.8'f32, 0.0'f32], + uv: [1.0'f32, 0.0'f32], + color: [0.0'f32, 1.0'f32, 0.25'f32, 1.0'f32] + ), + DxVertex( + position: [0.0'f32, 0.8'f32, 0.0'f32], + uv: [0.5'f32, 1.0'f32], + color: [0.25'f32, 0.0'f32, 1.0'f32, 1.0'f32] + ) + ] + + func controlTint(uv: Vec2, vertexColor: Vec4): Vec4 = + result = vertexColor + for i in 0 ..< 2: + result.x += uv.x * 0.025 + var steps = 0 + while steps < 2: + result.y += uv.y * 0.025 + inc steps + if result.w < 0.5: + result = vec4(0.0, 0.0, 0.0, 1.0) + + proc vertexMain( + position: Vec3, + uv: Vec2, + vertexColor: Vec4, + gl_Position: var Vec4, + fragmentUv: var Vec2, + fragmentColor: var Vec4 + ) = + gl_Position = vec4(position.x, position.y, position.z, 1.0) + fragmentUv = uv + fragmentColor = vertexColor + + proc pixelMain( + gl_FragCoord: Vec4, + fragmentUv: Vec2, + fragmentColor: Vec4, + fragColor: var Vec4 + ) = + fragColor = controlTint(fragmentUv, fragmentColor) + if gl_FragCoord.x < 0.0: + fragColor = vec4(0.0, 0.0, 0.0, 1.0) + + type DxRenderer = object + rootSignature: ID3D12RootSignature + pipelineState: ID3D12PipelineState + vertexBuffer: ID3D12Resource + vertexBufferView: D3D12_VERTEX_BUFFER_VIEW + + proc createVertexBuffer(ctx: var D3D12Context, renderer: var DxRenderer) = + let vertexBufferSize = UINT64(sizeof(DxVertices)) + + var bufferDesc: D3D12_RESOURCE_DESC + zeroMem(addr bufferDesc, sizeof(bufferDesc)) + bufferDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER + bufferDesc.Alignment = 0 + bufferDesc.Width = vertexBufferSize + bufferDesc.Height = 1 + bufferDesc.DepthOrArraySize = 1 + bufferDesc.MipLevels = 1 + bufferDesc.Format = DXGI_FORMAT_UNKNOWN + bufferDesc.SampleDesc = DXGI_SAMPLE_DESC(Count: 1, Quality: 0) + bufferDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR + bufferDesc.Flags = D3D12_RESOURCE_FLAG_NONE + + var defaultHeap: D3D12_HEAP_PROPERTIES + zeroMem(addr defaultHeap, sizeof(defaultHeap)) + defaultHeap.typ = D3D12_HEAP_TYPE_DEFAULT + defaultHeap.CPUPageProperty = 0 + defaultHeap.MemoryPoolPreference = 0 + defaultHeap.CreationNodeMask = 1 + defaultHeap.VisibleNodeMask = 1 + + var uploadHeap: D3D12_HEAP_PROPERTIES + zeroMem(addr uploadHeap, sizeof(uploadHeap)) + uploadHeap.typ = D3D12_HEAP_TYPE_UPLOAD + uploadHeap.CPUPageProperty = 0 + uploadHeap.MemoryPoolPreference = 0 + uploadHeap.CreationNodeMask = 1 + uploadHeap.VisibleNodeMask = 1 + + renderer.vertexBuffer = ctx.device.createCommittedResource( + addr defaultHeap, + D3D12_HEAP_FLAG_NONE, + addr bufferDesc, + D3D12_RESOURCE_STATE_COPY_DEST, + nil + ) + + let uploadBuffer = ctx.device.createCommittedResource( + addr uploadHeap, + D3D12_HEAP_FLAG_NONE, + addr bufferDesc, + D3D12_RESOURCE_STATE_GENERIC_READ, + nil + ) + + var uploadPtr: pointer + uploadBuffer.map(0, nil, addr uploadPtr) + copyMem(uploadPtr, unsafeAddr DxVertices[0], sizeof(DxVertices)) + uploadBuffer.unmap(0, nil) + + ctx.commandAllocator.reset() + ctx.commandList.reset(ctx.commandAllocator, nil) + ctx.commandList.copyBufferRegion( + renderer.vertexBuffer, + 0, + uploadBuffer, + 0, + vertexBufferSize + ) + + var barrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union( + Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: renderer.vertexBuffer, + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_COPY_DEST, + StateAfter: D3D12_RESOURCE_STATE_VERTEX_AND_CONSTANT_BUFFER + ) + ) + ) + ctx.commandList.resourceBarrier(1, addr barrier) + ctx.commandList.close() + + var cmdList = cast[ID3D12CommandList](ctx.commandList) + ctx.commandQueue.executeCommandLists(1, addr cmdList) + ctx.waitForGpu() + uploadBuffer.release() + + renderer.vertexBufferView = D3D12_VERTEX_BUFFER_VIEW( + BufferLocation: renderer.vertexBuffer.getGPUVirtualAddress(), + SizeInBytes: UINT(vertexBufferSize), + StrideInBytes: UINT(sizeof(DxVertex)) + ) + + proc initRenderer( + ctx: var D3D12Context, + renderer: var DxRenderer, + vertexSource, + pixelSource: string + ) = + let + vsBlob = compileShader(vertexSource, "VSMain", "vs_5_0") + psBlob = compileShader(pixelSource, "PSMain", "ps_5_0") + + doAssert getBufferSize(vsBlob) > 0 + doAssert getBufferSize(psBlob) > 0 + + var rootDesc = D3D12_ROOT_SIGNATURE_DESC( + NumParameters: 0, + pParameters: nil, + NumStaticSamplers: 0, + pStaticSamplers: nil, + Flags: D3D12_ROOT_SIGNATURE_FLAG_ALLOW_INPUT_ASSEMBLER_INPUT_LAYOUT + ) + let rootBlob = serializeRootSignature(addr rootDesc) + renderer.rootSignature = ctx.device.createRootSignature( + 0, + getBufferPointer(rootBlob), + getBufferSize(rootBlob) + ) + release(rootBlob) + + var inputElements = [ + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "POSITION", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32B32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 0, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "TEXCOORD", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 12, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ), + D3D12_INPUT_ELEMENT_DESC( + SemanticName: "COLOR", + SemanticIndex: 0, + Format: DXGI_FORMAT_R32G32B32A32_FLOAT, + InputSlot: 0, + AlignedByteOffset: 20, + InputSlotClass: D3D12_INPUT_CLASSIFICATION_PER_VERTEX_DATA, + InstanceDataStepRate: 0 + ) + ] + + var blendDesc: D3D12_BLEND_DESC + blendDesc.AlphaToCoverageEnable = 0 + blendDesc.IndependentBlendEnable = 0 + blendDesc.RenderTarget[0] = D3D12_RENDER_TARGET_BLEND_DESC( + BlendEnable: 0, + LogicOpEnable: 0, + SrcBlend: D3D12_BLEND_ONE, + DestBlend: D3D12_BLEND_ZERO, + BlendOp: D3D12_BLEND_OP_ADD, + SrcBlendAlpha: D3D12_BLEND_ONE, + DestBlendAlpha: D3D12_BLEND_ZERO, + BlendOpAlpha: D3D12_BLEND_OP_ADD, + LogicOp: 0, + RenderTargetWriteMask: uint8(D3D12_COLOR_WRITE_ENABLE_ALL) + ) + + let depthOp = D3D12_DEPTH_STENCILOP_DESC( + StencilFailOp: D3D12_STENCIL_OP_KEEP, + StencilDepthFailOp: D3D12_STENCIL_OP_KEEP, + StencilPassOp: D3D12_STENCIL_OP_KEEP, + StencilFunc: D3D12_COMPARISON_FUNC_ALWAYS + ) + + var psoDesc = D3D12_GRAPHICS_PIPELINE_STATE_DESC( + pRootSignature: renderer.rootSignature, + VS: shaderBytecode(vsBlob), + PS: shaderBytecode(psBlob), + StreamOutput: D3D12_STREAM_OUTPUT_DESC(), + BlendState: blendDesc, + SampleMask: D3D12_DEFAULT_SAMPLE_MASK, + RasterizerState: D3D12_RASTERIZER_DESC( + FillMode: D3D12_FILL_MODE_SOLID, + CullMode: D3D12_CULL_MODE_BACK, + FrontCounterClockwise: 0, + DepthBias: 0, + DepthBiasClamp: 0.0, + SlopeScaledDepthBias: 0.0, + DepthClipEnable: 1, + MultisampleEnable: 0, + AntialiasedLineEnable: 0, + ForcedSampleCount: 0, + ConservativeRaster: D3D12_CONSERVATIVE_RASTERIZATION_MODE_OFF + ), + DepthStencilState: D3D12_DEPTH_STENCIL_DESC( + DepthEnable: 0, + DepthWriteMask: D3D12_DEPTH_WRITE_MASK_ALL, + DepthFunc: D3D12_COMPARISON_FUNC_ALWAYS, + StencilEnable: 0, + StencilReadMask: 0xff'u8, + StencilWriteMask: 0xff'u8, + FrontFace: depthOp, + BackFace: depthOp + ), + InputLayout: D3D12_INPUT_LAYOUT_DESC( + pInputElementDescs: addr inputElements[0], + NumElements: uint32(inputElements.len) + ), + IBStripCutValue: 0, + PrimitiveTopologyType: D3D12_PRIMITIVE_TOPOLOGY_TYPE_TRIANGLE, + NumRenderTargets: 1, + DSVFormat: DXGI_FORMAT_UNKNOWN, + SampleDesc: DXGI_SAMPLE_DESC(Count: 1, Quality: 0), + NodeMask: 0, + CachedPSO: D3D12_CACHED_PIPELINE_STATE(), + Flags: 0 + ) + psoDesc.RTVFormats[0] = DXGI_FORMAT_R8G8B8A8_UNORM + + renderer.pipelineState = ctx.device.createGraphicsPipelineState(addr psoDesc) + + release(vsBlob) + release(psBlob) + createVertexBuffer(ctx, renderer) + + proc recordTriangle(ctx: var D3D12Context, renderer: DxRenderer) = + ctx.commandAllocator.reset() + ctx.commandList.reset(ctx.commandAllocator, renderer.pipelineState) + ctx.commandList.setGraphicsRootSignature(renderer.rootSignature) + ctx.commandList.setPipelineState(renderer.pipelineState) + + var barrier = D3D12_RESOURCE_BARRIER( + typ: D3D12_RESOURCE_BARRIER_TYPE_TRANSITION, + Flags: D3D12_RESOURCE_BARRIER_FLAG_NONE, + data: D3D12_RESOURCE_BARRIER_union( + Transition: D3D12_RESOURCE_TRANSITION_BARRIER( + pResource: ctx.renderTargets[ctx.currentFrame], + Subresource: D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES, + StateBefore: D3D12_RESOURCE_STATE_PRESENT, + StateAfter: D3D12_RESOURCE_STATE_RENDER_TARGET + ) + ) + ) + ctx.commandList.resourceBarrier(1, addr barrier) + ctx.commandList.rsSetViewports(1, addr ctx.viewport) + ctx.commandList.rsSetScissorRects(1, addr ctx.scissor) + ctx.commandList.omSetRenderTargets( + 1, + addr ctx.rtvHandles[ctx.currentFrame], + 1, + nil + ) + + let clearColor = [0.0.FLOAT, 0.0.FLOAT, 0.0.FLOAT, 1.0.FLOAT] + ctx.commandList.clearRenderTargetView( + ctx.rtvHandles[ctx.currentFrame], + unsafeAddr clearColor[0], + 0, + nil + ) + + ctx.commandList.iaSetPrimitiveTopology(D3D_PRIMITIVE_TOPOLOGY_TRIANGLELIST) + ctx.commandList.iaSetVertexBuffers( + 0, + 1, + unsafeAddr renderer.vertexBufferView + ) + ctx.commandList.drawInstanced(3, 1, 0, 0) + + barrier.data.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET + barrier.data.Transition.StateAfter = D3D12_RESOURCE_STATE_PRESENT + ctx.commandList.resourceBarrier(1, addr barrier) + ctx.commandList.close() + + proc shutdown(renderer: var DxRenderer) = + if renderer.vertexBuffer != nil: + renderer.vertexBuffer.release() + renderer.vertexBuffer = nil + if renderer.pipelineState != nil: + renderer.pipelineState.release() + renderer.pipelineState = nil + if renderer.rootSignature != nil: + renderer.rootSignature.release() + renderer.rootSignature = nil + + let + vertexSource = toHLSL(vertexMain, shaderVertex) + pixelSource = toHLSL(pixelMain, shaderFragment) + + let window = newWindow( + title = "Shady DirectX test", + size = ivec2(64, 64), + visible = false + ) + + var hwnd: HWND = window.getHWND() + if hwnd == 0: + raise newException(Exception, "Failed to acquire HWND from test window") + + var ctx: D3D12Context + ctx.initDevice(hwnd, 64, 64) + + var renderer: DxRenderer + try: + initRenderer(ctx, renderer, vertexSource, pixelSource) + recordTriangle(ctx, renderer) + ctx.executeFrame(vsync = false) + ctx.waitForGpu() + finally: + renderer.shutdown() + ctx.cleanup() + + echo "DirectX execution test passed" +elif defined(windows): + echo "DirectX test skipped; run with -d:shadyRunDx12 and dx12 on Nim path" +else: + echo "DirectX test skipped; Windows is required" diff --git a/tests/test_metal.nim b/tests/test_metal.nim new file mode 100644 index 0000000..37693d8 --- /dev/null +++ b/tests/test_metal.nim @@ -0,0 +1,45 @@ +when defined(macosx) and defined(shadyRunMetal): + import shady, vmath + import metal4 + + proc vertexMain( + vPos: Vec3, + vCol: Vec3, + gl_Position: var Vec4, + vertColor: var Vec3 + ) = + gl_Position = vec4(vPos.x, vPos.y, vPos.z, 1.0) + vertColor = vCol + + proc fragmentMain(fragColor: var Vec4) = + fragColor = vec4(1.0, 0.0, 0.0, 1.0) + + let device = MTLCreateSystemDefaultDevice() + checkNil(device, "Could not create a Metal device") + + var error: NSError + let vertexSource = toMSL(vertexMain, shaderVertex) + let vertexLibrary = device.newLibraryWithSource(@vertexSource, 0.ID, error.addr) + checkNSError(error, "Could not compile Shady Metal vertex shader") + checkNil(vertexLibrary, "Metal vertex library was nil") + checkNil( + vertexLibrary.newFunctionWithName(@"vertexMain"), + "Could not load generated Metal vertexMain" + ) + + error = 0.NSError + let fragmentSource = toMSL(fragmentMain, shaderFragment) + let fragmentLibrary = + device.newLibraryWithSource(@fragmentSource, 0.ID, error.addr) + checkNSError(error, "Could not compile Shady Metal fragment shader") + checkNil(fragmentLibrary, "Metal fragment library was nil") + checkNil( + fragmentLibrary.newFunctionWithName(@"fragmentMain"), + "Could not load generated Metal fragmentMain" + ) + + echo "Metal compiler test passed" +elif defined(macosx): + echo "Metal test skipped; run with -d:shadyRunMetal and metal4 on Nim path" +else: + echo "Metal test skipped; macOS is required" diff --git a/tests/test_opengl_glsl3.nim b/tests/test_opengl_glsl3.nim new file mode 100644 index 0000000..be839b3 --- /dev/null +++ b/tests/test_opengl_glsl3.nim @@ -0,0 +1,166 @@ +when defined(shadyRunOpenGL): + import opengl, shady, shady/compute, vmath + + proc compileStage(kind: GLenum, source, label: string): GLuint = + result = glCreateShader(kind) + var sourceArray = allocCStringArray([source]) + defer: deallocCStringArray(sourceArray) + glShaderSource(result, 1.GLsizei, sourceArray, nil) + glCompileShader(result) + + var ok: GLint + glGetShaderiv(result, GL_COMPILE_STATUS, ok.addr) + if ok == 0: + raise newException( + Exception, + label & " shader failed:\n" & + getErrorLog(result, label, glGetShaderiv, glGetShaderInfoLog) + ) + + proc compileProgram(vertexSource, fragmentSource: string): GLuint = + let vertexShader = compileStage(GL_VERTEX_SHADER, vertexSource, "glsl3.vert") + let fragmentShader = compileStage(GL_FRAGMENT_SHADER, fragmentSource, "glsl3.frag") + result = glCreateProgram() + glAttachShader(result, vertexShader) + glAttachShader(result, fragmentShader) + glLinkProgram(result) + + var ok: GLint + glGetProgramiv(result, GL_LINK_STATUS, ok.addr) + if ok == 0: + raise newException( + Exception, + "GLSL3 program failed:\n" & + getErrorLog(result, "glsl3.program", glGetProgramiv, glGetProgramInfoLog) + ) + + proc enableAttribute( + program: GLuint, + name: string, + size: GLint, + stride: GLsizei, + offset: int + ) = + let location = glGetAttribLocation(program, name.cstring) + doAssert location >= 0 + glEnableVertexAttribArray(location.GLuint) + glVertexAttribPointer( + location.GLuint, + size, + cGL_FLOAT, + GL_FALSE, + stride, + cast[pointer](offset) + ) + + var atlas: Uniform[Sampler2d] + + func controlTint(texColor, vertexColor: Vec4): Vec4 = + result = vec4( + texColor.x * vertexColor.x, + texColor.y * vertexColor.y, + texColor.z * vertexColor.z, + texColor.w * vertexColor.w + ) + for i in 0 ..< 2: + result.x += 0.05 + var steps = 0 + while steps < 2: + result.z += 0.125 + inc steps + if result.w < 0.5: + result = vec4(0.0, 0.0, 0.0, 1.0) + + proc layoutVertex( + position: Vec3, + uv: Vec2, + vertexColor: Vec4, + gl_Position: var Vec4, + fragmentUv: var Vec2, + fragmentColor: var Vec4 + ) = + gl_Position = vec4(position.x, position.y, position.z, 1.0) + fragmentUv = uv + fragmentColor = vertexColor + + proc texturedFragment( + fragmentUv: Vec2, + fragmentColor: Vec4, + fragColor: var Vec4 + ) = + let texColor = texture(atlas, fragmentUv) + fragColor = controlTint(texColor, fragmentColor) + + initOffscreenWindow(ivec2(32, 32)) + + let program = compileProgram( + toShader(layoutVertex, glsl3Desktop, shaderVertex), + toShader(texturedFragment, glsl3Desktop, shaderFragment) + ) + + var vertexArray: GLuint + glGenVertexArrays(1, vertexArray.addr) + glBindVertexArray(vertexArray) + + let vertices = [ + -1.0'f32, -1.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32, + 3.0'f32, -1.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32, + -1.0'f32, 3.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32 + ] + + var vertexBuffer: GLuint + glGenBuffers(1, vertexBuffer.addr) + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer) + glBufferData( + GL_ARRAY_BUFFER, + vertices.len * sizeof(float32), + unsafeAddr vertices[0], + GL_STATIC_DRAW + ) + + const stride = (9 * sizeof(float32)).GLsizei + enableAttribute(program, "position", 3, stride, 0) + enableAttribute(program, "uv", 2, stride, 3 * sizeof(float32)) + enableAttribute(program, "vertexColor", 4, stride, 5 * sizeof(float32)) + + var textureId: GLuint + var pixels = [0'u8, 255'u8, 0'u8, 255'u8] + glGenTextures(1, textureId.addr) + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, textureId) + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA.GLint, + 1, + 1, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + pixels[0].addr + ) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint) + + glViewport(0, 0, 32, 32) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glUseProgram(program) + let atlasLocation = glGetUniformLocation(program, "atlas") + doAssert atlasLocation >= 0 + glUniform1i(atlasLocation, 0) + glDrawArrays(GL_TRIANGLES, 0, 3) + glFinish() + + var pixel: array[4, uint8] + glReadPixels(16, 16, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel[0].addr) + doAssert pixel[0] in 15'u8 .. 45'u8 + doAssert pixel[1] in 170'u8 .. 210'u8 + doAssert pixel[2] in 45'u8 .. 85'u8 + doAssert pixel[3] > 200 + + echo "OpenGL GLSL3 execution test passed" +else: + echo "OpenGL GLSL3 execution test skipped; run with -d:shadyRunOpenGL" diff --git a/tests/test_opengl_glsl4.nim b/tests/test_opengl_glsl4.nim new file mode 100644 index 0000000..7f8bf54 --- /dev/null +++ b/tests/test_opengl_glsl4.nim @@ -0,0 +1,166 @@ +when defined(shadyRunOpenGL): + import opengl, shady, shady/compute, vmath + + proc compileStage(kind: GLenum, source, label: string): GLuint = + result = glCreateShader(kind) + var sourceArray = allocCStringArray([source]) + defer: deallocCStringArray(sourceArray) + glShaderSource(result, 1.GLsizei, sourceArray, nil) + glCompileShader(result) + + var ok: GLint + glGetShaderiv(result, GL_COMPILE_STATUS, ok.addr) + if ok == 0: + raise newException( + Exception, + label & " shader failed:\n" & + getErrorLog(result, label, glGetShaderiv, glGetShaderInfoLog) + ) + + proc compileProgram(vertexSource, fragmentSource: string): GLuint = + let vertexShader = compileStage(GL_VERTEX_SHADER, vertexSource, "glsl4.vert") + let fragmentShader = compileStage(GL_FRAGMENT_SHADER, fragmentSource, "glsl4.frag") + result = glCreateProgram() + glAttachShader(result, vertexShader) + glAttachShader(result, fragmentShader) + glLinkProgram(result) + + var ok: GLint + glGetProgramiv(result, GL_LINK_STATUS, ok.addr) + if ok == 0: + raise newException( + Exception, + "GLSL4 program failed:\n" & + getErrorLog(result, "glsl4.program", glGetProgramiv, glGetProgramInfoLog) + ) + + proc enableAttribute( + program: GLuint, + name: string, + size: GLint, + stride: GLsizei, + offset: int + ) = + let location = glGetAttribLocation(program, name.cstring) + doAssert location >= 0 + glEnableVertexAttribArray(location.GLuint) + glVertexAttribPointer( + location.GLuint, + size, + cGL_FLOAT, + GL_FALSE, + stride, + cast[pointer](offset) + ) + + var atlas: Uniform[Sampler2d] + + func controlTint(texColor, vertexColor: Vec4): Vec4 = + result = vec4( + texColor.x * vertexColor.x, + texColor.y * vertexColor.y, + texColor.z * vertexColor.z, + texColor.w * vertexColor.w + ) + for i in 0 ..< 2: + result.x += 0.05 + var steps = 0 + while steps < 2: + result.z += 0.125 + inc steps + if result.w < 0.5: + result = vec4(0.0, 0.0, 0.0, 1.0) + + proc layoutVertex( + position: Vec3, + uv: Vec2, + vertexColor: Vec4, + gl_Position: var Vec4, + fragmentUv: var Vec2, + fragmentColor: var Vec4 + ) = + gl_Position = vec4(position.x, position.y, position.z, 1.0) + fragmentUv = uv + fragmentColor = vertexColor + + proc texturedFragment( + fragmentUv: Vec2, + fragmentColor: Vec4, + fragColor: var Vec4 + ) = + let texColor = texture(atlas, fragmentUv) + fragColor = controlTint(texColor, fragmentColor) + + initOffscreenWindow(ivec2(32, 32)) + + let program = compileProgram( + toShader(layoutVertex, glsl4Desktop, shaderVertex), + toShader(texturedFragment, glsl4Desktop, shaderFragment) + ) + + var vertexArray: GLuint + glGenVertexArrays(1, vertexArray.addr) + glBindVertexArray(vertexArray) + + let vertices = [ + -1.0'f32, -1.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32, + 3.0'f32, -1.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32, + -1.0'f32, 3.0'f32, 0.0'f32, 0.5'f32, 0.5'f32, 1.0'f32, 0.75'f32, 1.0'f32, 1.0'f32 + ] + + var vertexBuffer: GLuint + glGenBuffers(1, vertexBuffer.addr) + glBindBuffer(GL_ARRAY_BUFFER, vertexBuffer) + glBufferData( + GL_ARRAY_BUFFER, + vertices.len * sizeof(float32), + unsafeAddr vertices[0], + GL_STATIC_DRAW + ) + + const stride = (9 * sizeof(float32)).GLsizei + enableAttribute(program, "position", 3, stride, 0) + enableAttribute(program, "uv", 2, stride, 3 * sizeof(float32)) + enableAttribute(program, "vertexColor", 4, stride, 5 * sizeof(float32)) + + var textureId: GLuint + var pixels = [0'u8, 255'u8, 0'u8, 255'u8] + glGenTextures(1, textureId.addr) + glActiveTexture(GL_TEXTURE0) + glBindTexture(GL_TEXTURE_2D, textureId) + glTexImage2D( + GL_TEXTURE_2D, + 0, + GL_RGBA.GLint, + 1, + 1, + 0, + GL_RGBA, + GL_UNSIGNED_BYTE, + pixels[0].addr + ) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE.GLint) + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE.GLint) + + glViewport(0, 0, 32, 32) + glClearColor(0, 0, 0, 1) + glClear(GL_COLOR_BUFFER_BIT) + glUseProgram(program) + let atlasLocation = glGetUniformLocation(program, "atlas") + doAssert atlasLocation >= 0 + glUniform1i(atlasLocation, 0) + glDrawArrays(GL_TRIANGLES, 0, 3) + glFinish() + + var pixel: array[4, uint8] + glReadPixels(16, 16, 1, 1, GL_RGBA, GL_UNSIGNED_BYTE, pixel[0].addr) + doAssert pixel[0] in 15'u8 .. 45'u8 + doAssert pixel[1] in 170'u8 .. 210'u8 + doAssert pixel[2] in 45'u8 .. 85'u8 + doAssert pixel[3] > 200 + + echo "OpenGL GLSL4 execution test passed" +else: + echo "OpenGL GLSL4 execution test skipped; run with -d:shadyRunOpenGL" From 2ee66ba1695787bc1034a95db8ca28f91451f3fd Mon Sep 17 00:00:00 2001 From: treeform Date: Tue, 26 May 2026 06:44:32 -0700 Subject: [PATCH 2/8] Improve HLSL resource binding support --- src/shady/backends/dx12.nim | 33 +++++++++++++-- src/shady/backends/shared.nim | 79 ++++++++++++++++++++++++++++------- tests/test_backends.nim | 22 ++++++++-- 3 files changed, 112 insertions(+), 22 deletions(-) diff --git a/src/shady/backends/dx12.nim b/src/shady/backends/dx12.nim index 34df59f..b1e32a8 100644 --- a/src/shady/backends/dx12.nim +++ b/src/shady/backends/dx12.nim @@ -1,6 +1,8 @@ import strutils, tables -type EntryParam* = tuple[name, typ: string, isOut: bool] +type + EntryParam* = tuple[name, typ: string, isOut: bool] + UniformParam* = tuple[name, typ: string] const hlslHeader* = "// target hlsl dx12\n" @@ -92,7 +94,7 @@ proc hlslTypeDefault*(t: string): string = proc semanticBase(name: string): string = let lower = name.toLowerAscii() - if name in ["gl_Position", "position"] or lower.endsWith("position"): + if name in ["gl_Position", "position", "pos"] or lower.endsWith("position"): "POSITION" elif name in ["gl_FragCoord"]: "SV_POSITION" @@ -137,8 +139,28 @@ proc nextSemanticIndex( proc hlslOutputFieldName*(name: string): string = if name == "gl_Position": "pos" else: name -proc hlslSamplerDecl*(name: string): string = - "SamplerState " & name & "Sampler;" +proc hlslTextureDecl*(name, typ: string, register: int): string = + typ & " " & name & " : register(t" & $register & ");" + +proc hlslSamplerDecl*(name: string, register: int): string = + "SamplerState " & name & "Sampler : register(s" & $register & ");" + +proc emitHlslUniformBuffer*( + uniforms: openArray[UniformParam], + register = 0 +): string = + if uniforms.len == 0: + return "" + result.add "cbuffer ShadyUniforms : register(b" + result.add $register + result.add ") {\n" + for uniform in uniforms: + result.add " " + result.add uniform.typ + result.add " " + result.add uniform.name + result.add ";\n" + result.add "};\n" proc hlslResourceCall*( name: string, @@ -264,6 +286,9 @@ proc emitHlslEntry*( result.add " PSMain(" var inputSemanticCounts: Table[string, int] var first = true + if not params.hasParam("gl_FragCoord"): + result.add "\n float4 gl_FragCoord : SV_POSITION" + first = false for p in params: if not p.isOut: if first: diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim index fe16b1a..ce8ac41 100644 --- a/src/shady/backends/shared.nim +++ b/src/shady/backends/shared.nim @@ -943,7 +943,10 @@ proc gatherFunction( topLevelNode: NimNode, functions: var Table[string, string], globals: var Table[string, string], - types: var Table[string, string] + types: var Table[string, string], + hlslUniforms: var seq[dx12.UniformParam], + hlslUniformNames: var Table[string, bool], + hlslTextures: var Table[string, int] ) = ## Looks for functions this function calls and brings them up @@ -963,6 +966,7 @@ proc gatherFunction( var defStr = "" let typeInst = n.getTypeInst gatherTypes(typeInst, types) + var addGlobal = true if typeInst.kind == nnkBracketExpr: # might be a uniform if typeInst[0].repr in ["Uniform", "UniformWriteOnly", "Attribute"]: @@ -974,7 +978,24 @@ proc gatherFunction( defStr.add "highp " defStr.add samplerType elif isHlsl(): - defStr.add samplerType + if typeInst[0].repr == "Uniform" and + typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]: + let textureRegister = + if name in hlslTextures: + hlslTextures[name] + else: + let nextRegister = hlslTextures.len + hlslTextures[name] = nextRegister + nextRegister + defStr.add hlslTextureDecl(name, samplerType, textureRegister) + globals[name & "Sampler"] = hlslSamplerDecl(name, textureRegister) + elif typeInst[0].repr == "Uniform": + if name notin hlslUniformNames: + hlslUniformNames[name] = true + hlslUniforms.add (name: name, typ: samplerType) + addGlobal = false + else: + defStr.add samplerType else: if typeInst[0].repr == "Uniform": defStr.add "constant " @@ -992,17 +1013,17 @@ proc gatherFunction( err "Invalid x[y].", n else: defStr.add typeRename(typeInst.repr) - defStr.add " " & name - if impl[2].kind != nnkEmpty: - defStr.add " = " & repr(impl[2]) - defStr.addSmart ';' - if defStr notin ["uniform Uniform = T;", + if addGlobal: + if not (isHlsl() and typeInst.kind == nnkBracketExpr and + typeInst[0].repr == "Uniform" and + typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]): + defStr.add " " & name + if impl[2].kind != nnkEmpty: + defStr.add " = " & repr(impl[2]) + defStr.addSmart ';' + if addGlobal and defStr notin ["uniform Uniform = T;", "attribute Attribute = T;"]: globals[name] = defStr - if isHlsl() and typeInst.kind == nnkBracketExpr and - typeInst[0].repr == "Uniform" and - typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]: - globals[name & "Sampler"] = hlslSamplerDecl(name) if n.kind == nnkCall: # Looking for functions. @@ -1016,7 +1037,15 @@ proc gatherFunction( not isVectorAccess(procName): ## If its not a builtin proc, we need to bring definition. let impl = n[0].getImpl() - gatherFunction(impl, functions, globals, types) + gatherFunction( + impl, + functions, + globals, + types, + hlslUniforms, + hlslUniformNames, + hlslTextures + ) functions[procName] = procDef(impl) if n.kind == nnkFormalParams: @@ -1025,7 +1054,15 @@ proc gatherFunction( let paramType = paramDef[^2].getTypeInst() gatherTypes(paramType, types) - gatherFunction(n, functions, globals, types) + gatherFunction( + n, + functions, + globals, + types, + hlslUniforms, + hlslUniformNames, + hlslTextures + ) proc toGLSLInner*(s: NimNode, version, extra: string): string = @@ -1048,7 +1085,18 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = var functions: Table[string, string] var globals: Table[string, string] var types: Table[string, string] - gatherFunction(n, functions, globals, types) + var hlslUniforms: seq[dx12.UniformParam] + var hlslUniformNames: Table[string, bool] + var hlslTextures: Table[string, int] + gatherFunction( + n, + functions, + globals, + types, + hlslUniforms, + hlslUniformNames, + hlslTextures + ) # Put types first. code.addGap() @@ -1058,6 +1106,9 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = # Put globals next. code.addGap() + if isHlsl() and hlslUniforms.len > 0: + code.add emitHlslUniformBuffer(hlslUniforms) + code.add "\n" for k, v in globals: code.add(v) code.add "\n" diff --git a/tests/test_backends.nim b/tests/test_backends.nim index a688aca..0849a06 100644 --- a/tests/test_backends.nim +++ b/tests/test_backends.nim @@ -18,7 +18,8 @@ block: doAssert "out vec4 fragColor;" in glsl4 let hlsl = toHLSL(fragmentConstant, shaderFragment) - doAssert "float4 PSMain()" in hlsl + doAssert "float4 PSMain(" in hlsl + doAssert "float4 gl_FragCoord : SV_POSITION" in hlsl doAssert ": SV_TARGET" in hlsl doAssert "float4 fragColor = float4(0.0, 0.0, 0.0, 0.0);" in hlsl doAssert "return fragColor;" in hlsl @@ -58,8 +59,8 @@ block: fragColor = texture(atlas, uv) let hlsl = toHLSL(sampledTexture, shaderFragment) - doAssert "Texture2D atlas;" in hlsl - doAssert "SamplerState atlasSampler;" in hlsl + doAssert "Texture2D atlas : register(t0);" in hlsl + doAssert "SamplerState atlasSampler : register(s0);" in hlsl doAssert "atlas.Sample(atlasSampler, uv)" in hlsl let msl = toMSL(sampledTexture, shaderFragment) @@ -68,6 +69,7 @@ block: block: var atlas: Uniform[Sampler2d] + var viewportSize: Uniform[Vec2] func controlTint(texColor, vertexColor: Vec4): Vec4 = result = vec4( @@ -132,7 +134,7 @@ block: doAssert "float4 fragmentColor : COLOR0" in hlslVertex let hlslFragment = toHLSL(layoutFragment, shaderFragment) - doAssert "Texture2D atlas;" in hlslFragment + doAssert "Texture2D atlas : register(t0);" in hlslFragment doAssert "atlas.Sample(atlasSampler, fragmentUv)" in hlslFragment doAssert "float4 controlTint" in hlslFragment doAssert "for(int i = 0; i < 2; i++)" in hlslFragment @@ -154,4 +156,16 @@ block: doAssert "while(steps < 2)" in mslFragment doAssert "if (" in mslFragment + proc uniformVertex( + pos: Vec2, + gl_Position: var Vec4 + ) = + let clip = pos / viewportSize + gl_Position = vec4(clip.x, clip.y, 0.0, 1.0) + + let hlslUniformVertex = toHLSL(uniformVertex, shaderVertex) + doAssert "cbuffer ShadyUniforms : register(b0)" in hlslUniformVertex + doAssert "float2 viewportSize;" in hlslUniformVertex + doAssert "float2 pos : POSITION0" in hlslUniformVertex + echo "Backend codegen tests passed" From 7f149144bd8d4293333d1aa1b0205098d873fe25 Mon Sep 17 00:00:00 2001 From: treeform Date: Tue, 26 May 2026 07:54:01 -0700 Subject: [PATCH 3/8] Add Vulkan GLSL and binary shader utilities --- src/shady.nim | 5 +- src/shady/backends/shared.nim | 206 +++++++++++++++++++++++++++++++--- src/shady/backends/vulkan.nim | 29 +++++ src/shady/binary.nim | 60 ++++++++++ tests/test_backends.nim | 21 ++++ 5 files changed, 301 insertions(+), 20 deletions(-) create mode 100644 src/shady/backends/vulkan.nim create mode 100644 src/shady/binary.nim diff --git a/src/shady.nim b/src/shady.nim index d21d82d..53b5696 100644 --- a/src/shady.nim +++ b/src/shady.nim @@ -1,5 +1,6 @@ ## Public switchboard for Shady's shader backends. -import shady/backends/[shared, glsl, glsl3, glsl4, dx12, metal4] +import shady/backends/[shared, glsl, glsl3, glsl4, dx12, metal4, vulkan] +import shady/binary -export shared, glsl, glsl3, glsl4, dx12, metal4 +export shared, glsl, glsl3, glsl4, dx12, metal4, vulkan, binary diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim index ce8ac41..75c19a8 100644 --- a/src/shady/backends/shared.nim +++ b/src/shady/backends/shared.nim @@ -1,7 +1,7 @@ ## Shader macro, converts Nim code into shader source. import macros, pixie, strutils, tables, vmath -import shady/backends/[glsl, glsl3, glsl4, dx12, metal4] +import shady/backends/[glsl, glsl3, glsl4, dx12, metal4, vulkan] from chroma import ColorRGBX type @@ -9,6 +9,7 @@ type glsl3WebGL ## OpenGL ES 3.0 / WebGL 2.0 compatibility profile. glsl3Desktop ## Desktop GLSL 3.30 / OpenGL 3.3 compatibility profile. glsl4Desktop ## Explicit desktop GLSL for OpenGL 4.1+. + vulkanGlsl450 ## GLSL 4.50 source shaped for Vulkan/SPIR-V. hlslDX12 ## HLSL for DirectX 12. metalMSL ## Metal Shading Language. @@ -33,6 +34,11 @@ var useResult {.compiletime.}: bool var shaderTarget* {.compiletime.}: ShaderTarget var shaderStage* {.compiletime.}: ShaderStage +const shaderSamplerTypes = [ + "Sampler2d", "SamplerCube", "Sampler2dShadow", "USampler2d", + "Sampler2dArray" +] + template glslTarget*(): untyped = shaderTarget @@ -41,7 +47,7 @@ proc err(msg: string, n: NimNode) {.noreturn.} = proc language(target: ShaderTarget): ShaderLanguage = case target - of glsl3WebGL, glsl3Desktop, glsl4Desktop: + of glsl3WebGL, glsl3Desktop, glsl4Desktop, vulkanGlsl450: langGlsl of hlslDX12: langHlsl @@ -51,6 +57,9 @@ proc language(target: ShaderTarget): ShaderLanguage = proc isGlsl(): bool = shaderTarget.language == langGlsl +proc isVulkan(): bool = + shaderTarget == vulkanGlsl450 + proc isHlsl(): bool = shaderTarget.language == langHlsl @@ -122,6 +131,26 @@ proc typeString(n: NimNode): string = err "can't figure out type: " & n.repr, n +proc arrayLength(n: NimNode): int = + if n.kind != nnkBracketExpr or n[0].repr != "array": + return 0 + if n[1].kind == nnkIntLit: + return n[1].intVal.int + if n[1].kind == nnkBracketExpr and n[1].len == 3 and n[1][0].repr == "..": + return n[1][2].intVal.int - n[1][1].intVal.int + 1 + if n[1].kind == nnkInfix and n[1].len == 3 and n[1][0].repr == "..": + return n[1][2].intVal.int - n[1][1].intVal.int + 1 + err "can't figure out array length: " & n.repr, n + +proc splitArrayType(n: NimNode): tuple[baseType, suffix: string] = + if n.kind == nnkBracketExpr and n[0].repr == "array": + let length = n.arrayLength() + result.baseType = typeString(n[2]) + result.suffix = "[" & $length & "]" + else: + result.baseType = typeString(n) + result.suffix = "" + ## Default constructor for different shader types. proc typeDefault(t: string, n: NimNode): string = result = @@ -248,7 +277,10 @@ proc emitBackendCall(n: NimNode, res: var string): bool = return false let name = n[0].strVal - if name notin ["texture", "texelFetch", "imageLoad", "imageStore"]: + if name notin [ + "texture", "textureLod", "textureSize", "texelFetch", "imageLoad", + "imageStore" + ]: return false var args: seq[string] for i in 1 ..< n.len: @@ -666,7 +698,7 @@ proc toCode(n: NimNode, res: var string, level = 0) = res.add "(" for subn in n: case subn.kind - of nnkElifExpr: + of nnkElifExpr, nnkElifBranch: if gotElse: echo n.treeRepr err "Cannot have elif after else", n @@ -675,7 +707,7 @@ proc toCode(n: NimNode, res: var string, level = 0) = res.add ") ? (" subn[1].toCode(res) res.add ") : " - of nnkElseExpr: + of nnkElseExpr, nnkElse: gotElse = true res.add "(" subn[0].toCode(res) @@ -779,6 +811,7 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = emitBackendEntry(params, body, res) return + var entryStage = shaderAuto for n in topLevelNode: case n.kind of nnkEmpty: @@ -788,6 +821,9 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = of nnkFormalParams: ## Main function parameters are different in they they go in as globals. res.addGap() + entryStage = resolvedStage(gatherEntryParams(n)) + var inLocation = 0 + var outLocation = 0 for paramDefs in n: if paramDefs.kind == nnkIdentDefs: let typeNode = paramDefs[^2] @@ -802,10 +838,20 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = continue elif typeNode[0].repr == "int": res.add "flat " + if isVulkan(): + res.add "layout(location = " + res.add $outLocation + res.add ") " + inc outLocation res.add "out " res.add typeRename(typeNode[0].strVal) else: if typeNode.kind == nnkBracketExpr: + if isVulkan(): + res.add "layout(location = " + res.add $inLocation + res.add ") " + inc inLocation res.add "in " res.add typeString(typeNode) else: @@ -813,6 +859,11 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = res.add "layout(origin_upper_left) " if typeNode.strVal == "int": res.add "flat " + if isVulkan(): + res.add "layout(location = " + res.add $inLocation + res.add ") " + inc inLocation res.add "in " res.add typeRename(typeNode.strVal) res.add " " @@ -822,7 +873,12 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = else: res.addGap() res.add "void main() {\n" + let oldUseResult = useResult + useResult = false n.toCodeStmts(res, level+1) + useResult = oldUseResult + if isVulkan() and entryStage == shaderVertex: + res.add " gl_Position.y = -gl_Position.y;\n" res.add "}\n" proc hasResult(node: NimNode): bool = @@ -946,7 +1002,10 @@ proc gatherFunction( types: var Table[string, string], hlslUniforms: var seq[dx12.UniformParam], hlslUniformNames: var Table[string, bool], - hlslTextures: var Table[string, int] + hlslTextures: var Table[string, int], + vulkanUniforms: var seq[vulkan.UniformParam], + vulkanUniformNames: var Table[string, bool], + vulkanTextures: var Table[string, int] ) = ## Looks for functions this function calls and brings them up @@ -970,8 +1029,29 @@ proc gatherFunction( if typeInst.kind == nnkBracketExpr: # might be a uniform if typeInst[0].repr in ["Uniform", "UniformWriteOnly", "Attribute"]: - let samplerType = typeString(typeInst[1]) - if isGlsl(): + let (samplerType, arraySuffix) = splitArrayType(typeInst[1]) + if isVulkan(): + if typeInst[0].repr == "Uniform" and + typeInst[1].repr in shaderSamplerTypes: + let textureBinding = + if name in vulkanTextures: + vulkanTextures[name] + else: + let nextBinding = vulkanTextures.len + vulkanTextures[name] = nextBinding + nextBinding + defStr.add vulkanSamplerDecl(name, samplerType, textureBinding) + elif typeInst[0].repr == "Uniform": + if name notin vulkanUniformNames: + vulkanUniformNames[name] = true + vulkanUniforms.add ( + name: name & arraySuffix, + typ: samplerType + ) + addGlobal = false + else: + defStr.add samplerType + elif isGlsl(): defStr.add typeRename(typeInst[0].repr) defStr.add " " if shaderTarget == glslES3 and glsl3NeedsHighp(samplerType): @@ -979,7 +1059,7 @@ proc gatherFunction( defStr.add samplerType elif isHlsl(): if typeInst[0].repr == "Uniform" and - typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]: + typeInst[1].repr in shaderSamplerTypes: let textureRegister = if name in hlslTextures: hlslTextures[name] @@ -992,7 +1072,10 @@ proc gatherFunction( elif typeInst[0].repr == "Uniform": if name notin hlslUniformNames: hlslUniformNames[name] = true - hlslUniforms.add (name: name, typ: samplerType) + hlslUniforms.add ( + name: name & arraySuffix, + typ: samplerType + ) addGlobal = false else: defStr.add samplerType @@ -1005,10 +1088,12 @@ proc gatherFunction( else: defStr.add samplerType elif typeInst[0].repr == "array": - defStr.add typeString(typeInst[2]) - defStr.add "[" - defStr.add typeRename(typeInst[1][2].repr) - defStr.add "]" + let (baseType, arraySuffix) = splitArrayType(typeInst) + defStr.add baseType + defStr.add " " + defStr.add name + defStr.add arraySuffix + addGlobal = false else: err "Invalid x[y].", n else: @@ -1016,8 +1101,15 @@ proc gatherFunction( if addGlobal: if not (isHlsl() and typeInst.kind == nnkBracketExpr and typeInst[0].repr == "Uniform" and - typeInst[1].repr in ["Sampler2d", "USampler2d", "Sampler2dArray"]): + typeInst[1].repr in shaderSamplerTypes) and + not (isVulkan() and typeInst.kind == nnkBracketExpr and + typeInst[0].repr == "Uniform" and + typeInst[1].repr in shaderSamplerTypes): defStr.add " " & name + if typeInst.kind == nnkBracketExpr and + typeInst[0].repr in ["Uniform", "UniformWriteOnly", "Attribute"]: + let (_, arraySuffix) = splitArrayType(typeInst[1]) + defStr.add arraySuffix if impl[2].kind != nnkEmpty: defStr.add " = " & repr(impl[2]) defStr.addSmart ';' @@ -1044,7 +1136,10 @@ proc gatherFunction( types, hlslUniforms, hlslUniformNames, - hlslTextures + hlslTextures, + vulkanUniforms, + vulkanUniformNames, + vulkanTextures ) functions[procName] = procDef(impl) @@ -1061,7 +1156,10 @@ proc gatherFunction( types, hlslUniforms, hlslUniformNames, - hlslTextures + hlslTextures, + vulkanUniforms, + vulkanUniformNames, + vulkanTextures ) proc toGLSLInner*(s: NimNode, version, extra: string): string = @@ -1088,6 +1186,9 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = var hlslUniforms: seq[dx12.UniformParam] var hlslUniformNames: Table[string, bool] var hlslTextures: Table[string, int] + var vulkanUniforms: seq[vulkan.UniformParam] + var vulkanUniformNames: Table[string, bool] + var vulkanTextures: Table[string, int] gatherFunction( n, functions, @@ -1095,7 +1196,10 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = types, hlslUniforms, hlslUniformNames, - hlslTextures + hlslTextures, + vulkanUniforms, + vulkanUniformNames, + vulkanTextures ) # Put types first. @@ -1109,6 +1213,9 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = if isHlsl() and hlslUniforms.len > 0: code.add emitHlslUniformBuffer(hlslUniforms) code.add "\n" + if isVulkan() and vulkanUniforms.len > 0: + code.add emitVulkanPushConstants(vulkanUniforms) + code.add "\n" for k, v in globals: code.add(v) code.add "\n" @@ -1144,6 +1251,8 @@ proc toGLSLInner*(s: NimNode, target: GlslTarget): string = case target of glsl4Desktop: toGLSLInner(s, glsl4DesktopVersion, glsl4DesktopExtra) + of vulkanGlsl450: + toGLSLInner(s, vulkanGlsl450Version, vulkanGlsl450Extra) of glsl3Desktop: toGLSLInner(s, glsl3DesktopVersion, glsl3DesktopExtra) of glsl3WebGL: @@ -1164,6 +1273,8 @@ proc toShaderInner*( case target of glsl4Desktop: toGLSLInner(s, glsl4DesktopVersion, glsl4DesktopExtra) + of vulkanGlsl450: + toGLSLInner(s, vulkanGlsl450Version, vulkanGlsl450Extra) of glsl3Desktop: toGLSLInner(s, glsl3DesktopVersion, glsl3DesktopExtra) of glsl3WebGL: @@ -1230,6 +1341,12 @@ type Sampler2d* = object image*: Image + SamplerCube* = object + faces*: array[6, Image] + + Sampler2dShadow* = object + image*: Image + USampler2d* = object image*: Image @@ -1240,6 +1357,37 @@ var ## GLSL globals. gl_Position*: Vec4 gl_VertexID*: int32 + gl_FrontFacing*: bool + +proc mat3*(m: Mat4): Mat3 = + result[0, 0] = m[0, 0] + result[0, 1] = m[0, 1] + result[0, 2] = m[0, 2] + result[1, 0] = m[1, 0] + result[1, 1] = m[1, 1] + result[1, 2] = m[1, 2] + result[2, 0] = m[2, 0] + result[2, 1] = m[2, 1] + result[2, 2] = m[2, 2] + +proc mat4*(diagonal: float32): Mat4 = + result[0, 0] = diagonal + result[1, 1] = diagonal + result[2, 2] = diagonal + result[3, 3] = diagonal + +proc `*`*(m: Mat4, s: float32): Mat4 = + for i in 0 ..< 4: + for j in 0 ..< 4: + result[i, j] = m[i, j] * s + +proc `*`*(s: float32, m: Mat4): Mat4 = + m * s + +proc `+`*(a, b: Mat4): Mat4 = + for i in 0 ..< 4: + for j in 0 ..< 4: + result[i, j] = a[i, j] + b[i, j] proc texelFetch*(buffer: Uniform[SamplerBuffer], index: SomeInteger): Vec4 = vec4(buffer.data[index.int], 0, 0, 0) @@ -1381,9 +1529,31 @@ proc texture*(buffer: Uniform[Sampler2D], pos: Vec2): Vec4 = ((pos.y mod 1.0) * buffer.image.height.float32) ).vec4() +proc texture*(buffer: Uniform[SamplerCube], pos: Vec3): Vec4 = + ## CPU stub for samplerCube; not used at runtime. Returns opaque black. + vec4(0, 0, 0, 1) + +proc texture*(buffer: Uniform[Sampler2dShadow], pos: Vec3): float32 = + ## CPU stub for sampler2DShadow; not used at runtime. Returns fully lit. + 1.0 + +proc textureLod*(buffer: Uniform[SamplerCube], pos: Vec3, lod: float32): Vec4 = + ## CPU stub for samplerCube textureLod; not used at runtime. + texture(buffer, pos) + +proc reflect*(incident, normal: Vec3): Vec3 = + incident - 2.0'f32 * dot(normal, incident) * normal + proc textureSize*(buffer: Uniform[Sampler2D], level: int): Vec2 = vec2(buffer.image.width.float32, buffer.image.height.float32) +proc textureSize*(buffer: Uniform[SamplerCube], level: int): Vec2 = + let image = buffer.faces[0] + vec2(image.width.float32, image.height.float32) + +proc textureSize*(buffer: Uniform[Sampler2dShadow], level: int): Vec2 = + vec2(buffer.image.width.float32, buffer.image.height.float32) + proc textureGrad*( s: Uniform[Sampler2D], uv: Vec3, diff --git a/src/shady/backends/vulkan.nim b/src/shady/backends/vulkan.nim new file mode 100644 index 0000000..3710d7c --- /dev/null +++ b/src/shady/backends/vulkan.nim @@ -0,0 +1,29 @@ +type UniformParam* = tuple[name, typ: string] + +const + vulkanGlsl450Version* = "450" + vulkanGlsl450Extra* = "" + +proc vulkanSamplerDecl*(name, typ: string, binding: int): string = + "layout(set = 0, binding = " & $binding & ") uniform " & typ & " " & + name & ";" + +proc emitVulkanPushConstants*( + uniforms: openArray[UniformParam] +): string = + if uniforms.len == 0: + return "" + result.add "layout(push_constant) uniform ShadyPushConstants {\n" + for uniform in uniforms: + result.add " " + result.add uniform.typ + result.add " " + result.add uniform.name + result.add ";\n" + result.add "} shadyPushConstants;\n" + for uniform in uniforms: + result.add "#define " + result.add uniform.name + result.add " shadyPushConstants." + result.add uniform.name + result.add "\n" diff --git a/src/shady/binary.nim b/src/shady/binary.nim new file mode 100644 index 0000000..cb1d348 --- /dev/null +++ b/src/shady/binary.nim @@ -0,0 +1,60 @@ +import std/[os, strutils] + +type BinaryShaderStage* = enum + binaryVertex + binaryFragment + binaryCompute + +proc quoteShell(path: string): string = + "\"" & path.replace("\"", "\\\"") & "\"" + +proc ensureParent(path: string) {.compileTime.} = + let dir = parentDir(path) + if dir.len > 0: + createDir(dir) + +proc spirvStage(stage: BinaryShaderStage): string = + case stage + of binaryVertex: "vert" + of binaryFragment: "frag" + of binaryCompute: "comp" + +proc writeShaderSource*( + source, sourcePath: string +): string {.compileTime.} = + ensureParent(sourcePath) + writeFile(sourcePath, source) + source + +proc compileSpirvShader*( + source, sourcePath, outputPath: string, + stage: BinaryShaderStage +): string {.compileTime.} = + discard writeShaderSource(source, sourcePath) + ensureParent(outputPath) + let compiler = + when defined(windows): + getEnv("SHADY_SPIRV_COMPILER", "glslangValidator.exe") + else: + getEnv("SHADY_SPIRV_COMPILER", "glslangValidator") + let command = + quoteShell(compiler) & " -V -S " & spirvStage(stage) & + " -o " & quoteShell(outputPath) & " " & quoteShell(sourcePath) + discard staticExec(command) + readFile(outputPath) + +proc compileHlslShader*( + source, sourcePath, outputPath, entryPoint, target: string +): string {.compileTime.} = + discard writeShaderSource(source, sourcePath) + ensureParent(outputPath) + let compiler = + when defined(windows): + getEnv("SHADY_HLSL_COMPILER", "dxc.exe") + else: + getEnv("SHADY_HLSL_COMPILER", "dxc") + let command = + quoteShell(compiler) & " -T " & target & " -E " & entryPoint & + " -Fo " & quoteShell(outputPath) & " " & quoteShell(sourcePath) + discard staticExec(command) + readFile(outputPath) diff --git a/tests/test_backends.nim b/tests/test_backends.nim index 0849a06..5d68ecc 100644 --- a/tests/test_backends.nim +++ b/tests/test_backends.nim @@ -126,6 +126,15 @@ block: doAssert "#version 410" in glsl4Vertex doAssert "in vec3 position;" in glsl4Vertex + let vulkanVertex = toShader(layoutVertex, vulkanGlsl450, shaderVertex) + doAssert "#version 450" in vulkanVertex + doAssert "layout(location = 0) in vec3 position;" in vulkanVertex + doAssert "layout(location = 1) in vec2 uv;" in vulkanVertex + doAssert "layout(location = 2) in vec4 vertexColor;" in vulkanVertex + doAssert "layout(location = 0) out vec2 fragmentUv;" in vulkanVertex + doAssert "layout(location = 1) out vec4 fragmentColor;" in vulkanVertex + doAssert "gl_Position.y = -gl_Position.y;" in vulkanVertex + let hlslVertex = toHLSL(layoutVertex, shaderVertex) doAssert "float3 position : POSITION0" in hlslVertex doAssert "float2 uv : TEXCOORD0" in hlslVertex @@ -168,4 +177,16 @@ block: doAssert "float2 viewportSize;" in hlslUniformVertex doAssert "float2 pos : POSITION0" in hlslUniformVertex + let vulkanUniformVertex = toShader(uniformVertex, vulkanGlsl450, shaderVertex) + doAssert "layout(push_constant) uniform ShadyPushConstants" in vulkanUniformVertex + doAssert "vec2 viewportSize;" in vulkanUniformVertex + doAssert "#define viewportSize shadyPushConstants.viewportSize" in vulkanUniformVertex + doAssert "layout(location = 0) in vec2 pos;" in vulkanUniformVertex + + let vulkanFragment = toShader(layoutFragment, vulkanGlsl450, shaderFragment) + doAssert "layout(set = 0, binding = 0) uniform sampler2D atlas;" in vulkanFragment + doAssert "layout(location = 0) in vec2 fragmentUv;" in vulkanFragment + doAssert "layout(location = 1) in vec4 fragmentColor;" in vulkanFragment + doAssert "layout(location = 0) out vec4 fragColor;" in vulkanFragment + echo "Backend codegen tests passed" From 3fc920e2ad69a06e25314ed6b5d32acf2b088e1d Mon Sep 17 00:00:00 2001 From: treeform Date: Tue, 26 May 2026 08:13:40 -0700 Subject: [PATCH 4/8] Extend backend codegen for glTF shaders --- src/shady/backends/dx12.nim | 40 ++++++++++++++++++++++++++++++----- src/shady/backends/glsl.nim | 10 ++++++--- src/shady/backends/glsl3.nim | 2 +- src/shady/backends/metal4.nim | 28 ++++++++++++++++++++++-- src/shady/backends/shared.nim | 23 +++++++++++++++++--- tests/test_backends.nim | 20 ++++++++++++++++++ 6 files changed, 109 insertions(+), 14 deletions(-) diff --git a/src/shady/backends/dx12.nim b/src/shady/backends/dx12.nim index b1e32a8..eb6ae9c 100644 --- a/src/shady/backends/dx12.nim +++ b/src/shady/backends/dx12.nim @@ -37,6 +37,8 @@ proc hlslTypeRename*(t: string): string = of "SamplerBuffer": "Buffer" of "Sampler2d": "Texture2D" + of "SamplerCube": "TextureCube" + of "Sampler2dShadow": "Texture2D" of "USampler2d": "Texture2D" of "Sampler2dArray": "Texture2DArray" of "ImageBuffer": "RWBuffer" @@ -62,12 +64,14 @@ proc hlslProcRename*(t: string): string = of "ivec4": "int4" of "mix": "lerp" of "fract": "frac" + of "atan": "atan2" of "dFdx": "ddx" of "dFdy": "ddy" of "fmod": "fmod" of "mod": "%" of "div": "/" of "gl_Position": "gl_Position" + of "gl_FrontFacing": "gl_FrontFacing" else: t.replace("`", "_") proc hlslTypeDefault*(t: string): string = @@ -113,6 +117,8 @@ proc hlslSemantic*(name: string, index: int, isOutput: bool): string = " : SV_POSITION" of "gl_FragCoord": " : SV_POSITION" + of "gl_FrontFacing": + " : SV_IsFrontFace" of "gl_VertexID": " : SV_VertexID" else: @@ -127,7 +133,8 @@ proc nextSemanticIndex( counts: var Table[string, int] ): int = case name - of "fragColor", "gl_FragColor", "gl_Position", "gl_FragCoord", "gl_VertexID": + of "fragColor", "gl_FragColor", "gl_Position", "gl_FragCoord", "gl_VertexID", + "gl_FrontFacing": return 0 else: let base = semanticBase(name) @@ -142,8 +149,13 @@ proc hlslOutputFieldName*(name: string): string = proc hlslTextureDecl*(name, typ: string, register: int): string = typ & " " & name & " : register(t" & $register & ");" -proc hlslSamplerDecl*(name: string, register: int): string = - "SamplerState " & name & "Sampler : register(s" & $register & ");" +proc hlslSamplerDecl*(name: string, register: int, nimType = ""): string = + let samplerType = + if nimType == "Sampler2dShadow": + "SamplerComparisonState" + else: + "SamplerState" + samplerType & " " & name & "Sampler : register(s" & $register & ");" proc emitHlslUniformBuffer*( uniforms: openArray[UniformParam], @@ -165,12 +177,23 @@ proc emitHlslUniformBuffer*( proc hlslResourceCall*( name: string, args: openArray[string], - firstArgIsSamplerBuffer: bool + firstArgIsSamplerBuffer: bool, + firstArgIsShadowSampler: bool ): string = case name of "texture": if args.len != 2: return "" - args[0] & ".Sample(" & args[0] & "Sampler, " & args[1] & ")" + if firstArgIsShadowSampler: + args[0] & ".SampleCmpLevelZero(" & args[0] & "Sampler, " & + args[1] & ".xy, " & args[1] & ".z)" + else: + args[0] & ".Sample(" & args[0] & "Sampler, " & args[1] & ")" + of "textureLod": + if args.len != 3: return "" + args[0] & ".SampleLevel(" & args[0] & "Sampler, " & args[1] & ", " & args[2] & ")" + of "textureSize": + if args.len < 1: return "" + args[0] & ".GetDimensions()" of "texelFetch": if args.len < 2: return "" if firstArgIsSamplerBuffer: @@ -289,6 +312,13 @@ proc emitHlslEntry*( if not params.hasParam("gl_FragCoord"): result.add "\n float4 gl_FragCoord : SV_POSITION" first = false + if not params.hasParam("gl_FrontFacing") and "gl_FrontFacing" in bodyCode: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " bool gl_FrontFacing : SV_IsFrontFace" for p in params: if not p.isOut: if first: diff --git a/src/shady/backends/glsl.nim b/src/shady/backends/glsl.nim index ddca541..bc1d662 100644 --- a/src/shady/backends/glsl.nim +++ b/src/shady/backends/glsl.nim @@ -34,6 +34,8 @@ proc glslTypeRename*(t: string): string = of "SamplerBuffer": "samplerBuffer" of "Sampler2d": "sampler2D" + of "SamplerCube": "samplerCube" + of "Sampler2dShadow": "sampler2DShadow" of "USampler2d": "usampler2D" of "Sampler2dArray": "sampler2DArray" of "ImageBuffer": "imageBuffer" @@ -73,7 +75,7 @@ proc glslTypeDefault*(t: string): string = const glslGlobals* = [ "gl_Position", "gl_FragCoord", "gl_GlobalInvocationID", - "gl_VertexID", + "gl_VertexID", "gl_FrontFacing", ] const glslFunctions* = [ @@ -86,13 +88,15 @@ const glslFunctions* = [ "IVec2", "IVec3", "IVec4", "abs", "clamp", "min", "max", "dot", "sqrt", "mix", "length", - "texelFetch", "imageStore", "imageLoad", "texture", "textureSize", + "cross", "reflect", "log2", + "texelFetch", "imageStore", "imageLoad", "texture", "textureLod", + "textureSize", "textureGrad", "normalize", "floor", "ceil", "round", "exp", "inversesqrt", "[]", "[]=", "inverse", - "sin", "cos", "tan", "pow", + "sin", "cos", "tan", "acos", "atan", "pow", "fmod", "lessThan", "lessThanEqual", "greaterThan", "greaterThanEqual", "equal", "notEqual", diff --git a/src/shady/backends/glsl3.nim b/src/shady/backends/glsl3.nim index a361db9..268ce50 100644 --- a/src/shady/backends/glsl3.nim +++ b/src/shady/backends/glsl3.nim @@ -6,6 +6,6 @@ const proc glsl3NeedsHighp*(samplerType: string): bool = samplerType in [ - "sampler2D", "usampler2D", "sampler2DArray", + "sampler2D", "samplerCube", "sampler2DShadow", "usampler2D", "sampler2DArray", "samplerBuffer", "imageBuffer", "uimageBuffer" ] diff --git a/src/shady/backends/metal4.nim b/src/shady/backends/metal4.nim index 438a305..d5a76ac 100644 --- a/src/shady/backends/metal4.nim +++ b/src/shady/backends/metal4.nim @@ -35,6 +35,8 @@ proc metalTypeRename*(t: string): string = of "SamplerBuffer": "texture_buffer" of "Sampler2d": "texture2d" + of "SamplerCube": "texturecube" + of "Sampler2dShadow": "depth2d" of "USampler2d": "texture2d" of "Sampler2dArray": "texture2d_array" of "ImageBuffer": "device float4*" @@ -58,6 +60,8 @@ proc metalProcRename*(t: string): string = of "ivec2": "int2" of "ivec3": "int3" of "ivec4": "int4" + of "mix": "mix" + of "atan": "atan2" of "dFdx": "dfdx" of "dFdy": "dfdy" of "fmod": "fmod" @@ -95,6 +99,8 @@ proc metalAttribute*(name: string, index: int, isOutput: bool): string = " [[position]]" of "gl_VertexID": " [[vertex_id]]" + of "gl_FrontFacing": + " [[front_facing]]" else: if isOutput: "" @@ -107,12 +113,23 @@ proc metalOutputFieldName*(name: string): string = proc metalResourceCall*( name: string, args: openArray[string], - firstArgIsSamplerBuffer: bool + firstArgIsSamplerBuffer: bool, + firstArgIsShadowSampler: bool ): string = case name of "texture": if args.len != 2: return "" - args[0] & ".sample(" & args[0] & "Sampler, " & args[1] & ")" + if firstArgIsShadowSampler: + args[0] & ".sample_compare(" & args[0] & "Sampler, " & + args[1] & ".xy, " & args[1] & ".z)" + else: + args[0] & ".sample(" & args[0] & "Sampler, " & args[1] & ")" + of "textureLod": + if args.len != 3: return "" + args[0] & ".sample(" & args[0] & "Sampler, " & args[1] & ", level(" & args[2] & "))" + of "textureSize": + if args.len < 1: return "" + args[0] & ".get_width()" of "texelFetch": if args.len < 2: return "" if firstArgIsSamplerBuffer: @@ -233,6 +250,13 @@ proc emitMetalEntry*( result.add p.name result.add metalAttribute(p.name, inputIndex, false) inc inputIndex + if not params.hasParam("gl_FrontFacing") and "gl_FrontFacing" in bodyCode: + if first: + result.add "\n" + first = false + else: + result.add ",\n" + result.add " bool gl_FrontFacing [[front_facing]]" if not first: result.add "\n" result.add ") {\n" diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim index 75c19a8..53981ac 100644 --- a/src/shady/backends/shared.nim +++ b/src/shady/backends/shared.nim @@ -268,6 +268,9 @@ proc typeInstRepr(n: NimNode): string = proc isSamplerBufferExpr(n: NimNode): bool = "SamplerBuffer" in n.typeInstRepr() +proc isShadowSamplerExpr(n: NimNode): bool = + "Sampler2dShadow" in n.typeInstRepr() + proc codeExpr(n: NimNode): string = n.toCode(result) @@ -288,9 +291,19 @@ proc emitBackendCall(n: NimNode, res: var string): bool = let call = if isHlsl(): - hlslResourceCall(name, args, n.len > 1 and n[1].isSamplerBufferExpr) + hlslResourceCall( + name, + args, + n.len > 1 and n[1].isSamplerBufferExpr, + n.len > 1 and n[1].isShadowSamplerExpr + ) else: - metalResourceCall(name, args, n.len > 1 and n[1].isSamplerBufferExpr) + metalResourceCall( + name, + args, + n.len > 1 and n[1].isSamplerBufferExpr, + n.len > 1 and n[1].isShadowSamplerExpr + ) if call.len == 0: return false res.add call @@ -1068,7 +1081,11 @@ proc gatherFunction( hlslTextures[name] = nextRegister nextRegister defStr.add hlslTextureDecl(name, samplerType, textureRegister) - globals[name & "Sampler"] = hlslSamplerDecl(name, textureRegister) + globals[name & "Sampler"] = hlslSamplerDecl( + name, + textureRegister, + typeInst[1].repr + ) elif typeInst[0].repr == "Uniform": if name notin hlslUniformNames: hlslUniformNames[name] = true diff --git a/tests/test_backends.nim b/tests/test_backends.nim index 5d68ecc..c4bdfa8 100644 --- a/tests/test_backends.nim +++ b/tests/test_backends.nim @@ -67,6 +67,26 @@ block: doAssert "texture2d atlas;" in msl doAssert "atlas.sample(atlasSampler, uv)" in msl +block: + var shadowMap: Uniform[Sampler2dShadow] + + proc sampledShadow(uvz: Vec3, fragColor: var Vec4) = + let lit = texture(shadowMap, uvz) + fragColor = vec4(lit, lit, lit, 1.0) + + let glsl = toShader(sampledShadow, glsl4Desktop, shaderFragment) + doAssert "uniform sampler2DShadow shadowMap;" in glsl + doAssert "texture(shadowMap, uvz)" in glsl + + let hlsl = toHLSL(sampledShadow, shaderFragment) + doAssert "Texture2D shadowMap : register(t0);" in hlsl + doAssert "SamplerComparisonState shadowMapSampler : register(s0);" in hlsl + doAssert "shadowMap.SampleCmpLevelZero(shadowMapSampler, uvz.xy, uvz.z)" in hlsl + + let msl = toMSL(sampledShadow, shaderFragment) + doAssert "depth2d shadowMap;" in msl + doAssert "shadowMap.sample_compare(shadowMapSampler, uvz.xy, uvz.z)" in msl + block: var atlas: Uniform[Sampler2d] var viewportSize: Uniform[Vec2] From a4a1703f020f46b557b78c05f45fffa2245941bd Mon Sep 17 00:00:00 2001 From: treeform Date: Tue, 26 May 2026 12:14:00 -0700 Subject: [PATCH 5/8] Add backend shader uniform support --- src/shady/backends/dx12.nim | 71 ++++++++++--------- src/shady/backends/shared.nim | 126 +++++++++++++++++++++++++++++++--- src/shady/backends/vulkan.nim | 93 ++++++++++++++++++++++++- 3 files changed, 248 insertions(+), 42 deletions(-) diff --git a/src/shady/backends/dx12.nim b/src/shady/backends/dx12.nim index eb6ae9c..1c90264 100644 --- a/src/shady/backends/dx12.nim +++ b/src/shady/backends/dx12.nim @@ -146,6 +146,9 @@ proc nextSemanticIndex( proc hlslOutputFieldName*(name: string): string = if name == "gl_Position": "pos" else: name +proc hlslInputFieldName*(name: string): string = + if name == "gl_FragCoord": "pos" else: name + proc hlslTextureDecl*(name, typ: string, register: int): string = typ & " " & name & " : register(t" & $register & ");" @@ -163,7 +166,9 @@ proc emitHlslUniformBuffer*( ): string = if uniforms.len == 0: return "" - result.add "cbuffer ShadyUniforms : register(b" + result.add "cbuffer ShadyUniforms" + result.add $register + result.add " : register(b" result.add $register result.add ") {\n" for uniform in uniforms: @@ -304,43 +309,45 @@ proc emitHlslEntry*( of 2: let outputIndex = firstOutput(params) let returnType = if outputIndex >= 0: params[outputIndex].typ else: "void" - result.add "\n" - result.add returnType - result.add " PSMain(" - var inputSemanticCounts: Table[string, int] - var first = true + var inputs: seq[EntryParam] if not params.hasParam("gl_FragCoord"): - result.add "\n float4 gl_FragCoord : SV_POSITION" - first = false - if not params.hasParam("gl_FrontFacing") and "gl_FrontFacing" in bodyCode: - if first: - result.add "\n" - first = false - else: - result.add ",\n" - result.add " bool gl_FrontFacing : SV_IsFrontFace" + inputs.add (name: "gl_FragCoord", typ: "float4", isOut: false) for p in params: if not p.isOut: - if first: - result.add "\n" - first = false - else: - result.add ",\n" - result.add " " - result.add p.typ - result.add " " - result.add p.name - result.add hlslSemantic( - p.name, - nextSemanticIndex(p.name, inputSemanticCounts), - false - ) - if not first: - result.add "\n" - result.add ")" + inputs.add p + if not params.hasParam("gl_FrontFacing") and "gl_FrontFacing" in bodyCode: + inputs.add (name: "gl_FrontFacing", typ: "bool", isOut: false) + + result.add "\nstruct PSInput {\n" + var inputSemanticCounts: Table[string, int] + for p in inputs: + result.add " " + result.add p.typ + result.add " " + result.add hlslInputFieldName(p.name) + result.add hlslSemantic( + p.name, + nextSemanticIndex(p.name, inputSemanticCounts), + false + ) + result.add ";\n" + result.add "};\n\n" + + result.add returnType + result.add " PSMain(PSInput input)" if outputIndex >= 0: result.add hlslSemantic(params[outputIndex].name, 0, true) result.add " {\n" + for p in inputs: + if p.name == "gl_FragCoord" and not params.hasParam("gl_FragCoord"): + continue + result.add " " + result.add p.typ + result.add " " + result.add p.name + result.add " = input." + result.add hlslInputFieldName(p.name) + result.add ";\n" result.add outputLocals(params, 1) result.add bodyCode if outputIndex >= 0: diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim index 53981ac..af14e2f 100644 --- a/src/shady/backends/shared.nim +++ b/src/shady/backends/shared.nim @@ -76,6 +76,22 @@ proc typeRename(t: string): string = of langMetal: metalTypeRename(t) +proc isMatrixTypeName(t: string): bool = + let renamed = typeRename(t) + t.contains("Mat") or renamed in [ + "float2x2", "float3x3", "float4x4", + "double2x2", "double3x3", "double4x4" + ] + +proc isVectorTypeName(t: string): bool = + let renamed = typeRename(t) + t.contains("Vec") or renamed in [ + "float2", "float3", "float4", + "double2", "double3", "double4", + "int2", "int3", "int4", + "uint2", "uint3", "uint4" + ] + proc typeString(n: NimNode): string = if n.kind != nnkBracketExpr: typeRename(n.strVal) @@ -214,6 +230,32 @@ proc getPrecedence(n: NimNode): int = else: -1 +proc looksMatrixExpr(n: NimNode): bool = + let r = n.repr + if r in ["skin", "model", "view", "proj", "lightSpace"] or + "jointMatrices" in r: + return true + if n.kind == nnkInfix and "*" in n[0].repr: + return n[1].looksMatrixExpr() or n[2].looksMatrixExpr() + false + +proc looksVectorExpr(n: NimNode): bool = + let r = n.repr + if r in [ + "vertexPosition", "vertexColor", "vertexNormal", "vertexUV", + "vertexTangent", "vertexWeights", "vertexUV1", "skinnedPosition", + "skinnedNormal", "skinnedTangent", "worldPos", "normal", "tangent", + "bitangent", "vPosLightSpace", "gl_Position" + ]: + return true + if n.kind in {nnkCall, nnkCommand}: + let name = procRename(n[0].strVal) + if name in ["float2", "float3", "float4", "double2", "double3", "double4"]: + return true + if n.kind == nnkDotExpr and n[1].repr in ["xy", "xyz", "rgba", "rgb"]: + return true + false + proc addIndent(res: var string, level: int) = ## Add indent (only if its needed). if res.len == 0: @@ -344,6 +386,42 @@ proc toCode(n: NimNode, res: var string, level = 0) = n[2].toCode(res) res.addSmart ';' + elif isHlsl() and "*" in n[0].repr: + let + leftType = n[1].getType.repr + rightType = n[2].getType.repr + leftIsMatrix = leftType.isMatrixTypeName() or n[1].looksMatrixExpr() + rightIsMatrix = rightType.isMatrixTypeName() or n[2].looksMatrixExpr() + leftIsVector = leftType.isVectorTypeName() or n[1].looksVectorExpr() + rightIsVector = rightType.isVectorTypeName() or n[2].looksVectorExpr() + if (leftIsMatrix and (rightIsMatrix or rightIsVector)) or + (leftIsVector and rightIsMatrix): + res.add "mul(" + n[1].toCode(res) + res.add ", " + n[2].toCode(res) + res.add ")" + else: + let + a = n.getPrecedence() + l = n[1].getPrecedence() + r = n[2].getPrecedence() + if l >= a: + res.add "(" + n[1].toCode(res) + res.add ")" + else: + n[1].toCode(res) + res.add " " + n[0].toCode(res) + res.add " " + if r >= a: + res.add "(" + n[2].toCode(res) + res.add ")" + else: + n[2].toCode(res) + else: let a = n.getPrecedence() @@ -429,9 +507,24 @@ proc toCode(n: NimNode, res: var string, level = 0) = else: res.add procName res.add "(" - for j in 1 ..< n.len: - if j != 1: res.add ", " - n[j].toCode(res) + if isHlsl() and n.len == 2 and procName in ["float2x2", "float3x3", "float4x4"]: + let size = + case procName + of "float2x2": 2 + of "float3x3": 3 + else: 4 + for row in 0 ..< size: + for col in 0 ..< size: + if row != 0 or col != 0: + res.add ", " + if row == col: + n[1].toCode(res) + else: + res.add "0.0" + else: + for j in 1 ..< n.len: + if j != 1: res.add ", " + n[j].toCode(res) res.add ")" of nnkDotExpr: @@ -803,6 +896,12 @@ proc emitBackendEntry(params: seq[EntryParam], body: NimNode, res: var string) = else: res.add metal4.emitMetalEntry(params, bodyCode, stage) +proc resolvedEntryStage(topLevelNode: NimNode): ShaderStage = + for n in topLevelNode: + if n.kind == nnkFormalParams: + return resolvedStage(gatherEntryParams(n)) + shaderFragment + proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = ## Top level block such as in and out params. ## Generates the main function (which is not like all the other functions) @@ -930,8 +1029,6 @@ proc procDef(topLevelNode: NimNode): string = let paramType = param.getTypeInst() if paramType.kind == nnkVarTy: # Process `x: var float` - if isGlsl() and paramType[0].strVal == "int": - paramsStr.add "flat " if isMetal(): paramsStr.add "thread " paramsStr.add typeRename(paramType[0].strVal) @@ -947,8 +1044,6 @@ proc procDef(topLevelNode: NimNode): string = paramsStr.add typeRename(paramType[1].strVal) else: # Just a simple `x: float` case. - if isGlsl() and paramType.strVal == "int": - paramsStr.add "flat " paramsStr.add typeRename(paramType.strVal) paramsStr.add " " paramsStr.add paramName @@ -1195,6 +1290,7 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = code.add "// from " & s.strVal & "\n" var n = getImpl(s) + let entryStage = resolvedEntryStage(n) # Gather all globals and functions, and globals and functions they use. var functions: Table[string, string] @@ -1228,10 +1324,22 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = # Put globals next. code.addGap() if isHlsl() and hlslUniforms.len > 0: - code.add emitHlslUniformBuffer(hlslUniforms) + code.add emitHlslUniformBuffer( + hlslUniforms, + if entryStage == shaderFragment: 1 else: 0 + ) code.add "\n" if isVulkan() and vulkanUniforms.len > 0: - code.add emitVulkanPushConstants(vulkanUniforms) + if useVulkanUniformBuffer(vulkanUniforms): + code.add emitVulkanUniformBuffer( + vulkanUniforms, + if entryStage == shaderFragment: + vulkanFragmentUniformBinding + else: + vulkanVertexUniformBinding + ) + else: + code.add emitVulkanPushConstants(vulkanUniforms) code.add "\n" for k, v in globals: code.add(v) diff --git a/src/shady/backends/vulkan.nim b/src/shady/backends/vulkan.nim index 3710d7c..d2c7e9f 100644 --- a/src/shady/backends/vulkan.nim +++ b/src/shady/backends/vulkan.nim @@ -1,8 +1,14 @@ +import strutils + type UniformParam* = tuple[name, typ: string] const vulkanGlsl450Version* = "450" vulkanGlsl450Extra* = "" + vulkanUniformSet* = 1 + vulkanVertexUniformBinding* = 0 + vulkanFragmentUniformBinding* = 1 + vulkanMaxPushConstantBytes* = 128 proc vulkanSamplerDecl*(name, typ: string, binding: int): string = "layout(set = 0, binding = " & $binding & ") uniform " & typ & " " & @@ -22,8 +28,93 @@ proc emitVulkanPushConstants*( result.add ";\n" result.add "} shadyPushConstants;\n" for uniform in uniforms: + let defineName = uniform.name.split("[", 1)[0] result.add "#define " - result.add uniform.name + result.add defineName result.add " shadyPushConstants." + result.add defineName + result.add "\n" + +proc emitVulkanUniformBuffer*( + uniforms: openArray[UniformParam], + binding: int +): string = + if uniforms.len == 0: + return "" + result.add "layout(set = " + result.add $vulkanUniformSet + result.add ", binding = " + result.add $binding + result.add ", std140) uniform ShadyUniforms" + result.add $binding + result.add " {\n" + for uniform in uniforms: + result.add " " + result.add uniform.typ + result.add " " result.add uniform.name + result.add ";\n" + result.add "} shadyUniforms" + result.add $binding + result.add ";\n" + for uniform in uniforms: + let defineName = uniform.name.split("[", 1)[0] + result.add "#define " + result.add defineName + result.add " shadyUniforms" + result.add $binding + result.add "." + result.add defineName result.add "\n" + +proc uniformArrayLength(name: string): int = + let openBracket = name.find('[') + if openBracket < 0: + return 1 + let closeBracket = name.find(']', openBracket + 1) + if closeBracket < 0: + return 1 + try: + parseInt(name[openBracket + 1 ..< closeBracket]) + except ValueError: + 1 + +proc uniformStd140Size(uniform: UniformParam): int = + let count = uniformArrayLength(uniform.name) + let elementSize = + case uniform.typ + of "mat4": 64 + of "mat3": 48 + of "mat2": 32 + of "vec4", "ivec4", "uvec4": 16 + of "vec3", "ivec3", "uvec3": 12 + of "vec2", "ivec2", "uvec2": 8 + else: 4 + if count > 1: + count * (((elementSize + 15) div 16) * 16) + else: + elementSize + +proc uniformStd140Align(uniform: UniformParam): int = + if uniformArrayLength(uniform.name) > 1: + return 16 + case uniform.typ + of "mat4", "mat3", "mat2", "vec4", "ivec4", "uvec4", + "vec3", "ivec3", "uvec3": + 16 + of "vec2", "ivec2", "uvec2": + 8 + else: + 4 + +proc align(value, alignment: int): int = + ((value + alignment - 1) div alignment) * alignment + +proc vulkanUniformBytes*(uniforms: openArray[UniformParam]): int = + for uniform in uniforms: + result = align(result, uniformStd140Align(uniform)) + result += uniformStd140Size(uniform) + result = align(result, 4) + +proc useVulkanUniformBuffer*(uniforms: openArray[UniformParam]): bool = + vulkanUniformBytes(uniforms) > vulkanMaxPushConstantBytes From f9ab26d946c32cd9da7c04394a1353a375a9dba5 Mon Sep 17 00:00:00 2001 From: treeform Date: Wed, 27 May 2026 08:23:11 -0700 Subject: [PATCH 6/8] keep nimble simple --- shady.nimble | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/shady.nimble b/shady.nimble index 97779c1..b095570 100644 --- a/shady.nimble +++ b/shady.nimble @@ -1,15 +1,10 @@ -version = "0.1.5" -author = "Andre von Houck" -description = "Nim to GPU shader language compiler and supporting utilities." -license = "MIT" - -srcDir = "src" - +version = "0.1.5" +author = "Andre von Houck" +description = "Nim to GPU shader language compiler and supporting utilities." +license = "MIT" + +srcDir = "src" + requires "nim >= 1.1.4" requires "vmath >= 2.0.1" requires "pixie >= 5.1.0" - -task test, "Run Shady's stable CPU and codegen tests": - exec "nim c -r tests/test_shady.nim" - exec "nim c -r tests/test_cpu.nim" - exec "nim c -r tests/test_backends.nim" From dc59b48b7f1d4754700b8107a3d2e7a2c7be6f43 Mon Sep 17 00:00:00 2001 From: treeform Date: Thu, 28 May 2026 14:22:27 -0700 Subject: [PATCH 7/8] fix metal shader output --- src/shady/backends/metal4.nim | 190 ++++++++++++++++++++++++++-------- src/shady/backends/shared.nim | 89 ++++++++++++++-- 2 files changed, 227 insertions(+), 52 deletions(-) diff --git a/src/shady/backends/metal4.nim b/src/shady/backends/metal4.nim index d5a76ac..81abfa3 100644 --- a/src/shady/backends/metal4.nim +++ b/src/shady/backends/metal4.nim @@ -1,6 +1,9 @@ -import strutils +import algorithm, strutils -type EntryParam* = tuple[name, typ: string, isOut: bool] +type + EntryParam* = tuple[name, typ: string, isOut: bool] + UniformParam* = tuple[name, typ: string] + TextureParam* = tuple[name, typ: string, binding: int, samplerBinding: int] const metalHeader* = "#include \nusing namespace metal;\n" @@ -63,7 +66,7 @@ proc metalProcRename*(t: string): string = of "mix": "mix" of "atan": "atan2" of "dFdx": "dfdx" - of "dFdy": "dfdy" + of "dFdy": "-dfdy" of "fmod": "fmod" of "mod": "%" of "div": "/" @@ -110,6 +113,41 @@ proc metalAttribute*(name: string, index: int, isOutput: bool): string = proc metalOutputFieldName*(name: string): string = if name == "gl_Position": "position" else: name +proc metalTextureDecl*( + name, + typ: string, + binding, + samplerBinding: int +): string = + typ & " " & name & " [[texture(" & $binding & ")]],\n" & + " sampler " & name & "Sampler [[sampler(" & $samplerBinding & ")]]" + +proc emitMetalUniformBuffer*( + uniforms: openArray[UniformParam], + bufferIndex: int +): string = + if uniforms.len == 0: + return "" + result.add "struct ShadyUniforms" + result.add $bufferIndex + result.add " {\n" + for uniform in uniforms: + result.add " " + result.add uniform.typ + result.add " " + result.add uniform.name + result.add ";\n" + result.add "};\n" + for uniform in uniforms: + let defineName = uniform.name.split("[", 1)[0] + result.add "#define " + result.add defineName + result.add " shadyUniforms" + result.add $bufferIndex + result.add "." + result.add defineName + result.add "\n" + proc metalResourceCall*( name: string, args: openArray[string], @@ -157,6 +195,9 @@ proc hasParam(params: openArray[EntryParam], name: string): bool = if p.name == name: return true +proc isSpecialInput(p: EntryParam): bool = + p.name in ["gl_VertexID", "gl_FrontFacing"] + proc outputLocals(params: openArray[EntryParam], level: int): string = let indent = repeat(" ", level) for p in params: @@ -169,10 +210,90 @@ proc outputLocals(params: openArray[EntryParam], level: int): string = result.add metalTypeDefault(p.typ) result.add ";\n" +proc inputParams(params: openArray[EntryParam]): seq[EntryParam] = + for p in params: + if not p.isOut and not p.isSpecialInput(): + result.add p + +proc inputStruct( + name: string, + params: openArray[EntryParam], + stage: int +): string = + let inputs = params.inputParams() + if inputs.len == 0: + return "" + result.add "\nstruct " + result.add name + result.add " {\n" + for i, p in inputs: + result.add " " + result.add p.typ + result.add " " + result.add p.name + if stage == 1: + result.add metalAttribute(p.name, i, false) + result.add ";\n" + result.add "};\n" + +proc inputLocals( + params: openArray[EntryParam], + inputName: string, + level: int +): string = + let indent = repeat(" ", level) + for p in params.inputParams(): + result.add indent + result.add p.typ + result.add " " + result.add p.name + result.add " = " + result.add inputName + result.add "." + result.add p.name + result.add ";\n" + +proc addComma(result: var string, first: var bool) = + if first: + result.add "\n" + first = false + else: + result.add ",\n" + +proc resourceParams( + uniforms: openArray[UniformParam], + textures: openArray[TextureParam], + stage: int +): seq[string] = + let uniformBuffer = + if stage == 2: + 1 + else: + 0 + if uniforms.len > 0: + result.add "constant ShadyUniforms" & $uniformBuffer & + " &shadyUniforms" & $uniformBuffer & + " [[buffer(" & $uniformBuffer & ")]]" + + var textureList = @textures + textureList.sort( + proc(a, b: TextureParam): int = + cmp(a.binding, b.binding) + ) + for texture in textureList: + result.add metalTextureDecl( + texture.name, + texture.typ, + texture.binding, + texture.samplerBinding + ) + proc emitMetalEntry*( params: openArray[EntryParam], bodyCode: string, - stage: int + stage: int, + uniforms: openArray[UniformParam] = [], + textures: openArray[TextureParam] = [] ): string = case stage of 1: @@ -190,34 +311,25 @@ proc emitMetalEntry*( result.add metalAttribute(p.name, 0, true) result.add ";\n" result.add "};\n\n" + result.add inputStruct("VertexIn", params, stage) result.add "vertex VertexOut vertexMain(" - var inputIndex = 0 var first = true - for p in params: - if not p.isOut: - if first: - result.add "\n" - first = false - else: - result.add ",\n" - result.add " " - result.add p.typ - result.add " " - result.add p.name - result.add metalAttribute(p.name, inputIndex, false) - inc inputIndex + if params.inputParams().len > 0: + result.addComma(first) + result.add " VertexIn input [[stage_in]]" if not params.hasParam("gl_VertexID") and "gl_VertexID" in bodyCode: - if first: - result.add "\n" - first = false - else: - result.add ",\n" + result.addComma(first) result.add " uint gl_VertexID [[vertex_id]]" + for resource in resourceParams(uniforms, textures, stage): + result.addComma(first) + result.add " " + result.add resource if not first: result.add "\n" result.add ") {\n" result.add " VertexOut output;\n" + result.add inputLocals(params, "input", 1) result.add outputLocals(params, 1) result.add bodyCode for p in outputs: @@ -226,40 +338,34 @@ proc emitMetalEntry*( result.add " = " result.add p.name result.add ";\n" + if p.name == "gl_Position": + result.add " output.position.z = " + result.add "(output.position.z + output.position.w) * 0.5;\n" result.add " return output;\n" result.add "}\n" of 2: let outputIndex = firstOutput(params) let returnType = if outputIndex >= 0: params[outputIndex].typ else: "void" + result.add inputStruct("FragmentIn", params, stage) result.add "\nfragment " result.add returnType result.add " fragmentMain(" - var inputIndex = 0 var first = true - for p in params: - if not p.isOut: - if first: - result.add "\n" - first = false - else: - result.add ",\n" - result.add " " - result.add p.typ - result.add " " - result.add p.name - result.add metalAttribute(p.name, inputIndex, false) - inc inputIndex + if params.inputParams().len > 0: + result.addComma(first) + result.add " FragmentIn input [[stage_in]]" if not params.hasParam("gl_FrontFacing") and "gl_FrontFacing" in bodyCode: - if first: - result.add "\n" - first = false - else: - result.add ",\n" + result.addComma(first) result.add " bool gl_FrontFacing [[front_facing]]" + for resource in resourceParams(uniforms, textures, stage): + result.addComma(first) + result.add " " + result.add resource if not first: result.add "\n" result.add ") {\n" + result.add inputLocals(params, "input", 1) result.add outputLocals(params, 1) result.add bodyCode if outputIndex >= 0: diff --git a/src/shady/backends/shared.nim b/src/shady/backends/shared.nim index af14e2f..7a9b165 100644 --- a/src/shady/backends/shared.nim +++ b/src/shady/backends/shared.nim @@ -887,14 +887,26 @@ proc stageId(stage: ShaderStage): int = of shaderCompute: 3 of shaderAuto: 0 -proc emitBackendEntry(params: seq[EntryParam], body: NimNode, res: var string) = +proc emitBackendEntry( + params: seq[EntryParam], + body: NimNode, + res: var string, + metalUniforms: seq[metal4.UniformParam], + metalTextures: seq[metal4.TextureParam] +) = var bodyCode = "" body.toCodeStmts(bodyCode, 1) let stage = resolvedStage(params).stageId() if isHlsl(): res.add dx12.emitHlslEntry(params, bodyCode, stage) else: - res.add metal4.emitMetalEntry(params, bodyCode, stage) + res.add metal4.emitMetalEntry( + params, + bodyCode, + stage, + metalUniforms, + metalTextures + ) proc resolvedEntryStage(topLevelNode: NimNode): ShaderStage = for n in topLevelNode: @@ -902,7 +914,13 @@ proc resolvedEntryStage(topLevelNode: NimNode): ShaderStage = return resolvedStage(gatherEntryParams(n)) shaderFragment -proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = +proc toCodeTopLevel( + topLevelNode: NimNode, + res: var string, + level = 0, + metalUniforms: seq[metal4.UniformParam] = @[], + metalTextures: seq[metal4.TextureParam] = @[] +) = ## Top level block such as in and out params. ## Generates the main function (which is not like all the other functions) @@ -920,7 +938,7 @@ proc toCodeTopLevel(topLevelNode: NimNode, res: var string, level = 0) = else: body = n let params = gatherEntryParams(formalParams) - emitBackendEntry(params, body, res) + emitBackendEntry(params, body, res, metalUniforms, metalTextures) return var entryStage = shaderAuto @@ -1113,7 +1131,11 @@ proc gatherFunction( hlslTextures: var Table[string, int], vulkanUniforms: var seq[vulkan.UniformParam], vulkanUniformNames: var Table[string, bool], - vulkanTextures: var Table[string, int] + vulkanTextures: var Table[string, int], + metalUniforms: var seq[metal4.UniformParam], + metalUniformNames: var Table[string, bool], + metalTextures: var seq[metal4.TextureParam], + metalTextureNames: var Table[string, int] ) = ## Looks for functions this function calls and brings them up @@ -1191,6 +1213,31 @@ proc gatherFunction( addGlobal = false else: defStr.add samplerType + elif isMetal(): + if typeInst[0].repr == "Uniform" and + typeInst[1].repr in shaderSamplerTypes: + if name notin metalTextureNames: + let binding = metalTextures.len + metalTextureNames[name] = binding + metalTextures.add ( + name: name, + typ: samplerType, + binding: binding, + samplerBinding: binding + ) + addGlobal = false + elif typeInst[0].repr == "Uniform": + if name notin metalUniformNames: + metalUniformNames[name] = true + metalUniforms.add ( + name: name & arraySuffix, + typ: samplerType + ) + addGlobal = false + elif typeInst[0].repr == "UniformWriteOnly": + defStr.add samplerType + else: + defStr.add samplerType else: if typeInst[0].repr == "Uniform": defStr.add "constant " @@ -1251,7 +1298,11 @@ proc gatherFunction( hlslTextures, vulkanUniforms, vulkanUniformNames, - vulkanTextures + vulkanTextures, + metalUniforms, + metalUniformNames, + metalTextures, + metalTextureNames ) functions[procName] = procDef(impl) @@ -1271,7 +1322,11 @@ proc gatherFunction( hlslTextures, vulkanUniforms, vulkanUniformNames, - vulkanTextures + vulkanTextures, + metalUniforms, + metalUniformNames, + metalTextures, + metalTextureNames ) proc toGLSLInner*(s: NimNode, version, extra: string): string = @@ -1302,6 +1357,10 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = var vulkanUniforms: seq[vulkan.UniformParam] var vulkanUniformNames: Table[string, bool] var vulkanTextures: Table[string, int] + var metalUniforms: seq[metal4.UniformParam] + var metalUniformNames: Table[string, bool] + var metalTextures: seq[metal4.TextureParam] + var metalTextureNames: Table[string, int] gatherFunction( n, functions, @@ -1312,7 +1371,11 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = hlslTextures, vulkanUniforms, vulkanUniformNames, - vulkanTextures + vulkanTextures, + metalUniforms, + metalUniformNames, + metalTextures, + metalTextureNames ) # Put types first. @@ -1341,6 +1404,12 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = else: code.add emitVulkanPushConstants(vulkanUniforms) code.add "\n" + if isMetal() and metalUniforms.len > 0: + code.add emitMetalUniformBuffer( + metalUniforms, + if entryStage == shaderFragment: 1 else: 0 + ) + code.add "\n" for k, v in globals: code.add(v) code.add "\n" @@ -1365,7 +1434,7 @@ proc toGLSLInner*(s: NimNode, version, extra: string): string = code.add "\n" # Put the main function last. - toCodeTopLevel(n, code) + toCodeTopLevel(n, code, 0, metalUniforms, metalTextures) return code @@ -1667,7 +1736,7 @@ proc textureLod*(buffer: Uniform[SamplerCube], pos: Vec3, lod: float32): Vec4 = texture(buffer, pos) proc reflect*(incident, normal: Vec3): Vec3 = - incident - 2.0'f32 * dot(normal, incident) * normal + incident - 2.0'f * dot(normal, incident) * normal proc textureSize*(buffer: Uniform[Sampler2D], level: int): Vec2 = vec2(buffer.image.width.float32, buffer.image.height.float32) From aac2ceb581f6b64cd40a719f0a9ca33b2bb16886 Mon Sep 17 00:00:00 2001 From: treeform Date: Fri, 29 May 2026 10:26:56 -0700 Subject: [PATCH 8/8] fix metal point size --- src/shady/backends/metal4.nim | 2 ++ tests/test_backends.nim | 12 +++++++----- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/shady/backends/metal4.nim b/src/shady/backends/metal4.nim index 81abfa3..71b6292 100644 --- a/src/shady/backends/metal4.nim +++ b/src/shady/backends/metal4.nim @@ -303,6 +303,7 @@ proc emitMetalEntry*( outputs.add p result.add "\nstruct VertexOut {\n" + result.add " float pointSize [[point_size]];\n" for p in outputs: result.add " " result.add p.typ @@ -329,6 +330,7 @@ proc emitMetalEntry*( result.add "\n" result.add ") {\n" result.add " VertexOut output;\n" + result.add " output.pointSize = 1.0;\n" result.add inputLocals(params, "input", 1) result.add outputLocals(params, 1) result.add bodyCode diff --git a/tests/test_backends.nim b/tests/test_backends.nim index c4bdfa8..acc1453 100644 --- a/tests/test_backends.nim +++ b/tests/test_backends.nim @@ -19,7 +19,7 @@ block: let hlsl = toHLSL(fragmentConstant, shaderFragment) doAssert "float4 PSMain(" in hlsl - doAssert "float4 gl_FragCoord : SV_POSITION" in hlsl + doAssert "float4 pos : SV_POSITION;" in hlsl doAssert ": SV_TARGET" in hlsl doAssert "float4 fragColor = float4(0.0, 0.0, 0.0, 0.0);" in hlsl doAssert "return fragColor;" in hlsl @@ -49,6 +49,8 @@ block: let msl = toMSL(vertexPass, shaderVertex) doAssert "struct VertexOut" in msl doAssert "vertex VertexOut vertexMain(" in msl + doAssert "float pointSize [[point_size]];" in msl + doAssert "output.pointSize = 1.0;" in msl doAssert "float4 position [[position]];" in msl doAssert "output.position = gl_Position;" in msl @@ -64,7 +66,7 @@ block: doAssert "atlas.Sample(atlasSampler, uv)" in hlsl let msl = toMSL(sampledTexture, shaderFragment) - doAssert "texture2d atlas;" in msl + doAssert "texture2d atlas [[texture(0)]]" in msl doAssert "atlas.sample(atlasSampler, uv)" in msl block: @@ -84,7 +86,7 @@ block: doAssert "shadowMap.SampleCmpLevelZero(shadowMapSampler, uvz.xy, uvz.z)" in hlsl let msl = toMSL(sampledShadow, shaderFragment) - doAssert "depth2d shadowMap;" in msl + doAssert "depth2d shadowMap [[texture(0)]]" in msl doAssert "shadowMap.sample_compare(shadowMapSampler, uvz.xy, uvz.z)" in msl block: @@ -178,7 +180,7 @@ block: doAssert "float4 fragmentColor;" in mslVertex let mslFragment = toMSL(layoutFragment, shaderFragment) - doAssert "texture2d atlas;" in mslFragment + doAssert "texture2d atlas [[texture(0)]]" in mslFragment doAssert "atlas.sample(atlasSampler, fragmentUv)" in mslFragment doAssert "float4 controlTint" in mslFragment doAssert "for(int i = 0; i < 2; i++)" in mslFragment @@ -193,7 +195,7 @@ block: gl_Position = vec4(clip.x, clip.y, 0.0, 1.0) let hlslUniformVertex = toHLSL(uniformVertex, shaderVertex) - doAssert "cbuffer ShadyUniforms : register(b0)" in hlslUniformVertex + doAssert "cbuffer ShadyUniforms0 : register(b0)" in hlslUniformVertex doAssert "float2 viewportSize;" in hlslUniformVertex doAssert "float2 pos : POSITION0" in hlslUniformVertex