#include "../gfx/common.hpp" #include "../internal.hpp" #include "../webgpu/gpu.hpp" #include "gx.hpp" #include "gx_fmt.hpp" #include "shader_info.hpp" #include #include #include #include #include #include "tracy/Tracy.hpp" namespace aurora::gx { using namespace fmt::literals; using namespace std::string_literals; using namespace std::string_view_literals; static Module Log("aurora::gfx::gx"); absl::flat_hash_set g_seenShaders; static inline std::string_view chan_comp(GXTevColorChan chan) noexcept { switch (chan) { case GX_CH_RED: return "r"; case GX_CH_GREEN: return "g"; case GX_CH_BLUE: return "b"; case GX_CH_ALPHA: return "a"; default: return "?"; } } static bool is_alpha_bump_channel(GXChannelID id) noexcept { return id == GX_ALPHA_BUMP || id == GX_ALPHA_BUMPN; } static std::string tev_mask_expr(const std::string& value, u32 mask) { // t_IndTexCoord is already expanded into the 0..255 indirect sample domain. return fmt::format("(f32(u32({}) & 0x{:X}u) / 255.0)", value, mask); } static std::string alpha_bump_sel(size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { if (stage.indTexStage >= config.numIndStages || stage.indTexAlphaSel == GX_ITBA_OFF) { return "0.0"; } std::string baseCoord; switch (stage.indTexAlphaSel) { DEFAULT_FATAL("invalid indTexAlphaSel {} for stage {}", underlying(stage.indTexAlphaSel), stageIdx); case GX_ITBA_S: baseCoord = fmt::format("t_IndTexCoord{}.x", underlying(stage.indTexStage)); break; case GX_ITBA_T: baseCoord = fmt::format("t_IndTexCoord{}.y", underlying(stage.indTexStage)); break; case GX_ITBA_U: baseCoord = fmt::format("t_IndTexCoord{}.z", underlying(stage.indTexStage)); break; case GX_ITBA_OFF: return "0.0"; } switch (stage.indTexFormat) { DEFAULT_FATAL("invalid indirect format {} for stage {}", underlying(stage.indTexFormat), stageIdx); case GX_ITF_8: return tev_mask_expr(baseCoord, 0xF8u); case GX_ITF_5: return tev_mask_expr(baseCoord, 0xE0u); case GX_ITF_4: return tev_mask_expr(baseCoord, 0xF0u); case GX_ITF_3: return tev_mask_expr(baseCoord, 0xF8u); } } static bool uses_texture_sample(const TevStage& stage) noexcept { const auto& c = stage.colorPass; const auto& a = stage.alphaPass; return c.a == GX_CC_TEXC || c.a == GX_CC_TEXA || c.b == GX_CC_TEXC || c.b == GX_CC_TEXA || c.c == GX_CC_TEXC || c.c == GX_CC_TEXA || c.d == GX_CC_TEXC || c.d == GX_CC_TEXA || a.a == GX_CA_TEXA || a.b == GX_CA_TEXA || a.c == GX_CA_TEXA || a.d == GX_CA_TEXA; } u8 color_channel(GXChannelID id) noexcept { switch (id) { DEFAULT_FATAL("unimplemented color channel {}", id); case GX_COLOR0: case GX_ALPHA0: case GX_COLOR0A0: return 0; case GX_COLOR1: case GX_ALPHA1: case GX_COLOR1A1: return 1; } } static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { switch (arg) { DEFAULT_FATAL("invalid color arg {}", underlying(arg)); case GX_CC_CPREV: return "prev.rgb"; case GX_CC_APREV: return "vec3f(prev.a)"; case GX_CC_C0: return "tevreg0.rgb"; case GX_CC_A0: return "vec3f(tevreg0.a)"; case GX_CC_C1: return "tevreg1.rgb"; case GX_CC_A1: return "vec3f(tevreg1.a)"; case GX_CC_C2: return "tevreg2.rgb"; case GX_CC_A2: return "vec3f(tevreg2.a)"; case GX_CC_TEXC: { CHECK(stage.texMapId != GX_TEXMAP_NULL, "unmapped texture for stage {}", stageIdx); CHECK(stage.texMapId >= GX_TEXMAP0 && stage.texMapId <= GX_TEXMAP7, "invalid texture {} for stage {}", underlying(stage.texMapId), stageIdx); const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format("sampled{}.{}{}{}", stageIdx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); } case GX_CC_TEXA: { CHECK(stage.texMapId != GX_TEXMAP_NULL, "unmapped texture for stage {}", stageIdx); CHECK(stage.texMapId >= GX_TEXMAP0 && stage.texMapId <= GX_TEXMAP7, "invalid texture {} for stage {}", underlying(stage.texMapId), stageIdx); const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format("vec3f(sampled{}.{})", stageIdx, chan_comp(swap.alpha)); } case GX_CC_RASC: { // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { return "vec3f(0.0)"; } if (is_alpha_bump_channel(stage.channelId)) { std::string alpha = alpha_bump_sel(stageIdx, config, stage); if (stage.channelId == GX_ALPHA_BUMPN) { alpha = fmt::format("({} * (255.0 / 248.0))", alpha); } return fmt::format("vec3f({})", alpha); } u32 idx = color_channel(stage.channelId); const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format("rast{}.{}{}{}", idx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); } case GX_CC_RASA: { // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { return "vec3f(0.0)"; } if (is_alpha_bump_channel(stage.channelId)) { std::string alpha = alpha_bump_sel(stageIdx, config, stage); if (stage.channelId == GX_ALPHA_BUMPN) { alpha = fmt::format("({} * (255.0 / 248.0))", alpha); } return fmt::format("vec3f({})", alpha); } u32 idx = color_channel(stage.channelId); const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format("vec3f(rast{}.{})", idx, chan_comp(swap.alpha)); } case GX_CC_ONE: return "vec3f(1.0)"; case GX_CC_HALF: return "vec3f(0.5)"; case GX_CC_KONST: { switch (stage.kcSel) { DEFAULT_FATAL("invalid kcSel {}", underlying(stage.kcSel)); case GX_TEV_KCSEL_8_8: return "vec3f(1.0)"; case GX_TEV_KCSEL_7_8: return "vec3f(7.0/8.0)"; case GX_TEV_KCSEL_6_8: return "vec3f(6.0/8.0)"; case GX_TEV_KCSEL_5_8: return "vec3f(5.0/8.0)"; case GX_TEV_KCSEL_4_8: return "vec3f(4.0/8.0)"; case GX_TEV_KCSEL_3_8: return "vec3f(3.0/8.0)"; case GX_TEV_KCSEL_2_8: return "vec3f(2.0/8.0)"; case GX_TEV_KCSEL_1_8: return "vec3f(1.0/8.0)"; case GX_TEV_KCSEL_K0: return "ubuf.kcolor0.rgb"; case GX_TEV_KCSEL_K1: return "ubuf.kcolor1.rgb"; case GX_TEV_KCSEL_K2: return "ubuf.kcolor2.rgb"; case GX_TEV_KCSEL_K3: return "ubuf.kcolor3.rgb"; case GX_TEV_KCSEL_K0_R: return "vec3f(ubuf.kcolor0.r)"; case GX_TEV_KCSEL_K1_R: return "vec3f(ubuf.kcolor1.r)"; case GX_TEV_KCSEL_K2_R: return "vec3f(ubuf.kcolor2.r)"; case GX_TEV_KCSEL_K3_R: return "vec3f(ubuf.kcolor3.r)"; case GX_TEV_KCSEL_K0_G: return "vec3f(ubuf.kcolor0.g)"; case GX_TEV_KCSEL_K1_G: return "vec3f(ubuf.kcolor1.g)"; case GX_TEV_KCSEL_K2_G: return "vec3f(ubuf.kcolor2.g)"; case GX_TEV_KCSEL_K3_G: return "vec3f(ubuf.kcolor3.g)"; case GX_TEV_KCSEL_K0_B: return "vec3f(ubuf.kcolor0.b)"; case GX_TEV_KCSEL_K1_B: return "vec3f(ubuf.kcolor1.b)"; case GX_TEV_KCSEL_K2_B: return "vec3f(ubuf.kcolor2.b)"; case GX_TEV_KCSEL_K3_B: return "vec3f(ubuf.kcolor3.b)"; case GX_TEV_KCSEL_K0_A: return "vec3f(ubuf.kcolor0.a)"; case GX_TEV_KCSEL_K1_A: return "vec3f(ubuf.kcolor1.a)"; case GX_TEV_KCSEL_K2_A: return "vec3f(ubuf.kcolor2.a)"; case GX_TEV_KCSEL_K3_A: return "vec3f(ubuf.kcolor3.a)"; } } case GX_CC_ZERO: return "vec3f(0.0)"; } } static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { switch (arg) { DEFAULT_FATAL("invalid alpha arg {}", underlying(arg)); case GX_CA_APREV: return "prev.a"; case GX_CA_A0: return "tevreg0.a"; case GX_CA_A1: return "tevreg1.a"; case GX_CA_A2: return "tevreg2.a"; case GX_CA_TEXA: { CHECK(stage.texMapId != GX_TEXMAP_NULL, "unmapped texture for stage {}", stageIdx); CHECK(stage.texMapId >= GX_TEXMAP0 && stage.texMapId <= GX_TEXMAP7, "invalid texture {} for stage {}", underlying(stage.texMapId), stageIdx); const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format("sampled{}.{}", stageIdx, chan_comp(swap.alpha)); } case GX_CA_RASA: { // CHECK(stage.channelId != GX_COLOR_NULL, "unmapped color channel for stage {}", stageIdx); if (stage.channelId == GX_COLOR_ZERO || stage.channelId == GX_COLOR_NULL) { return "0.0"; } if (is_alpha_bump_channel(stage.channelId)) { std::string alpha = alpha_bump_sel(stageIdx, config, stage); if (stage.channelId == GX_ALPHA_BUMPN) { alpha = fmt::format("({} * (255.0 / 248.0))", alpha); } return alpha; } u32 idx = color_channel(stage.channelId); const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format("rast{}.{}", idx, chan_comp(swap.alpha)); } case GX_CA_KONST: { switch (stage.kaSel) { DEFAULT_FATAL("invalid kaSel {}", underlying(stage.kaSel)); case GX_TEV_KASEL_8_8: return "1.0"; case GX_TEV_KASEL_7_8: return "(7.0/8.0)"; case GX_TEV_KASEL_6_8: return "(6.0/8.0)"; case GX_TEV_KASEL_5_8: return "(5.0/8.0)"; case GX_TEV_KASEL_4_8: return "(4.0/8.0)"; case GX_TEV_KASEL_3_8: return "(3.0/8.0)"; case GX_TEV_KASEL_2_8: return "(2.0/8.0)"; case GX_TEV_KASEL_1_8: return "(1.0/8.0)"; case GX_TEV_KASEL_K0_R: return "ubuf.kcolor0.r"; case GX_TEV_KASEL_K1_R: return "ubuf.kcolor1.r"; case GX_TEV_KASEL_K2_R: return "ubuf.kcolor2.r"; case GX_TEV_KASEL_K3_R: return "ubuf.kcolor3.r"; case GX_TEV_KASEL_K0_G: return "ubuf.kcolor0.g"; case GX_TEV_KASEL_K1_G: return "ubuf.kcolor1.g"; case GX_TEV_KASEL_K2_G: return "ubuf.kcolor2.g"; case GX_TEV_KASEL_K3_G: return "ubuf.kcolor3.g"; case GX_TEV_KASEL_K0_B: return "ubuf.kcolor0.b"; case GX_TEV_KASEL_K1_B: return "ubuf.kcolor1.b"; case GX_TEV_KASEL_K2_B: return "ubuf.kcolor2.b"; case GX_TEV_KASEL_K3_B: return "ubuf.kcolor3.b"; case GX_TEV_KASEL_K0_A: return "ubuf.kcolor0.a"; case GX_TEV_KASEL_K1_A: return "ubuf.kcolor1.a"; case GX_TEV_KASEL_K2_A: return "ubuf.kcolor2.a"; case GX_TEV_KASEL_K3_A: return "ubuf.kcolor3.a"; } } case GX_CA_ZERO: return "0.0"; } } static std::string tev_op(GXTevOp op, std::string_view bias, std::string_view scale, std::string_view a, std::string_view b, std::string_view c, std::string_view d, std::string_view zero) { switch (op) { DEFAULT_FATAL("unimplemented tev op {}", underlying(op)); case GX_TEV_ADD: case GX_TEV_SUB: { std::string_view neg = op == GX_TEV_SUB ? "-"sv : ""sv; return fmt::format("(({0}mix({1}, {2}, {3}) + {4}){5}){6}", neg, a, b, c, d, bias, scale); } case GX_TEV_COMP_R8_GT: return fmt::format("select({3}, {2}, round({0}.r * 255.0) > round({1}.r * 255.0)) + {4}", a, b, c, zero, d); case GX_TEV_COMP_R8_EQ: return fmt::format("select({3}, {2}, round({0}.r * 255.0) == round({1}.r * 255.0)) + {4}", a, b, c, zero, d); case GX_TEV_COMP_GR16_GT: return fmt::format( "select({3}, {2}, round(dot({0}.rg * 255.0, vec2(1.0, 256.0))) > round(dot({1}.rg * 255.0, vec2(1.0, 256.0))))" " + {4}", a, b, c, zero, d); case GX_TEV_COMP_GR16_EQ: return fmt::format( "select({3}, {2}, round(dot({0}.rg * 255.0, vec2(1.0, 256.0))) == round(dot({1}.rg * 255.0, vec2(1.0, 256.0))))" " + {4}", a, b, c, zero, d); case GX_TEV_COMP_BGR24_GT: return fmt::format( "select({3}, {2}, round(dot({0}.rgb * 255.0, vec3(1.0, 256.0, 65536.0))) > round(dot({1}.rgb * 255.0, " "vec3(1.0, 256.0, 65536.0)))) + {4}", a, b, c, zero, d); case GX_TEV_COMP_BGR24_EQ: return fmt::format( "select({3}, {2}, round(dot({0}.rgb * 255.0, vec3(1.0, 256.0, 65536.0))) == round(dot({1}.rgb * 255.0, " "vec3(1.0, 256.0, 65536.0)))) + {4}", a, b, c, zero, d); case GX_TEV_COMP_RGB8_GT: return fmt::format("select({3}, {2}, round({0} * 255.0) > round({1} * 255.0)) + {4}", a, b, c, zero, d); case GX_TEV_COMP_RGB8_EQ: return fmt::format("select({3}, {2}, round({0} * 255.0) == round({1} * 255.0)) + {4}", a, b, c, zero, d); } } static std::string tev_color_op(GXTevOp op, std::string_view bias, std::string_view scale, bool clamp, std::string_view a, std::string_view b, std::string_view c, std::string_view d) { const auto overflow = [](std::string_view reg) { return fmt::format("tev_overflow_vec3f({})", reg); }; std::string expr = tev_op(op, bias, scale, overflow(a), overflow(b), overflow(c), d, "vec3(0)"sv); return clamp ? fmt::format("clamp({}, vec3f(0.0), vec3f(1.0))", expr) : fmt::format("clamp({}, vec3f(-4.0), vec3f(4.0))", expr); } static std::string tev_alpha_op(GXTevOp op, std::string_view bias, std::string_view scale, bool clamp, std::string_view a, std::string_view b, std::string_view c, std::string_view d) { const auto overflow = [](std::string_view reg) { return fmt::format("tev_overflow_f32({})", reg); }; std::string expr = tev_op(op, bias, scale, overflow(a), overflow(b), overflow(c), d, "0.0"sv); return clamp ? fmt::format("clamp({}, 0.0, 1.0)", expr) : fmt::format("clamp({}, -4.0, 4.0)", expr); } static std::string_view tev_bias(GXTevBias bias) { switch (bias) { DEFAULT_FATAL("invalid tev bias {}", underlying(bias)); case GX_TB_ZERO: return ""sv; case GX_TB_ADDHALF: return " + 0.5"sv; case GX_TB_SUBHALF: return " - 0.5"sv; } } struct AlphaCompareExpr { std::string expr; int constant = -1; }; static AlphaCompareExpr alpha_compare_const(bool value) { return {value ? "true"s : "false"s, value ? 1 : 0}; } static AlphaCompareExpr alpha_compare_not(const AlphaCompareExpr& expr) { if (expr.constant != -1) { return alpha_compare_const(expr.constant == 0); } return {fmt::format("!{}", expr.expr), -1}; } static AlphaCompareExpr alpha_compare_and(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { if (lhs.constant == 0 || rhs.constant == 0) { return alpha_compare_const(false); } if (lhs.constant == 1) { return rhs; } if (rhs.constant == 1) { return lhs; } return {fmt::format("({} && {})", lhs.expr, rhs.expr), -1}; } static AlphaCompareExpr alpha_compare_or(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { if (lhs.constant == 1 || rhs.constant == 1) { return alpha_compare_const(true); } if (lhs.constant == 0) { return rhs; } if (rhs.constant == 0) { return lhs; } return {fmt::format("({} || {})", lhs.expr, rhs.expr), -1}; } static AlphaCompareExpr alpha_compare_xor(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { if (lhs.constant != -1 && rhs.constant != -1) { return alpha_compare_const(lhs.constant != rhs.constant); } if (lhs.constant == 0) { return rhs; } if (rhs.constant == 0) { return lhs; } if (lhs.constant == 1) { return alpha_compare_not(rhs); } if (rhs.constant == 1) { return alpha_compare_not(lhs); } return {fmt::format("({} != {})", lhs.expr, rhs.expr), -1}; } static AlphaCompareExpr alpha_compare_xnor(const AlphaCompareExpr& lhs, const AlphaCompareExpr& rhs) { if (lhs.constant != -1 && rhs.constant != -1) { return alpha_compare_const(lhs.constant == rhs.constant); } if (lhs.constant == 0) { return alpha_compare_not(rhs); } if (rhs.constant == 0) { return alpha_compare_not(lhs); } if (lhs.constant == 1) { return rhs; } if (rhs.constant == 1) { return lhs; } return {fmt::format("({} == {})", lhs.expr, rhs.expr), -1}; } static AlphaCompareExpr alpha_compare(GXCompare comp, u8 ref) { const auto iref = static_cast(ref); switch (comp) { DEFAULT_FATAL("invalid alpha comp {}", underlying(comp)); case GX_NEVER: return alpha_compare_const(false); case GX_LESS: if (ref == 0) { return alpha_compare_const(false); } return {fmt::format("(alphaCompare < {}u)", iref), -1}; case GX_LEQUAL: if (ref == 255) { return alpha_compare_const(true); } return {fmt::format("(alphaCompare <= {}u)", iref), -1}; case GX_EQUAL: return {fmt::format("(alphaCompare == {}u)", iref), -1}; case GX_NEQUAL: return {fmt::format("(alphaCompare != {}u)", iref), -1}; case GX_GEQUAL: if (ref == 0) { return alpha_compare_const(true); } return {fmt::format("(alphaCompare >= {}u)", iref), -1}; case GX_GREATER: if (ref == 255) { return alpha_compare_const(false); } return {fmt::format("(alphaCompare > {}u)", iref), -1}; case GX_ALWAYS: return alpha_compare_const(true); } } static std::string_view tev_scale(GXTevScale scale) { switch (scale) { DEFAULT_FATAL("invalid tev scale {}", underlying(scale)); case GX_CS_SCALE_1: return ""sv; case GX_CS_SCALE_2: return " * 2.0"sv; case GX_CS_SCALE_4: return " * 4.0"sv; case GX_CS_DIVIDE_2: return " / 2.0"sv; } } static inline std::string vtx_attr(const ShaderConfig& config, GXAttr attr) { const auto type = config.attrs[attr].attrType; if (type == GX_NONE) { if (attr == GX_VA_PNMTXIDX) { return "ubuf.current_pnmtx"; } if (attr == GX_VA_NRM) { // Default normal return "vec3f(1.0, 0.0, 0.0)"s; } if (attr == GX_VA_CLR0 || attr == GX_VA_CLR1) { return "vec4f(0.0, 0.0, 0.0, 0.0)"s; } UNLIKELY FATAL("unmapped vtx attr {}", underlying(attr)); } if (attr == GX_VA_POS) { return "in_pos"s; } if (attr == GX_VA_NRM) { return "in_nrm"s; } if (attr == GX_VA_CLR0 || attr == GX_VA_CLR1) { const auto idx = attr - GX_VA_CLR0; return fmt::format("in_clr{}", idx); } if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { const auto idx = attr - GX_VA_TEX0; return fmt::format("in_tex{}_uv", idx); } if (attr == GX_VA_PNMTXIDX) { return "in_pnmtxidx"s; } if (attr >= GX_VA_TEX0MTXIDX && attr <= GX_VA_TEX7MTXIDX) { const auto idx = attr - GX_VA_TEX0MTXIDX; return fmt::format("in_texmtxidx{}", idx); } UNLIKELY FATAL("unhandled vtx attr {}", underlying(attr)); } constexpr std::array TevColorArgNames{ "CPREV"sv, "APREV"sv, "C0"sv, "A0"sv, "C1"sv, "A1"sv, "C2"sv, "A2"sv, "TEXC"sv, "TEXA"sv, "RASC"sv, "RASA"sv, "ONE"sv, "HALF"sv, "KONST"sv, "ZERO"sv, }; constexpr std::array TevAlphaArgNames{ "APREV"sv, "A0"sv, "A1"sv, "A2"sv, "TEXA"sv, "RASA"sv, "KONST"sv, "ZERO"sv, }; auto fetch_fixed16_attr(std::string_view fetchFn, const AttrConfig& mapping, std::string_view buf, std::string_view offs, bool le) -> std::string { // Some Adreno drivers appear sensitive to generated shaders that route 2- and // 3-component fixed-16 vertex attributes through reusable vector fetch helpers. // Emitting scalar component fetches at the call site avoids observed artifacts. if (mapping.cnt == 2) { const auto comp0 = fmt::format("{}_1(&{}, {} + 0u, {}u, {})", fetchFn, buf, offs, mapping.frac, le); const auto comp1 = fmt::format("{}_1(&{}, {} + 2u, {}u, {})", fetchFn, buf, offs, mapping.frac, le); return fmt::format("vec2f({}, {})", comp0, comp1); } if (mapping.cnt == 3) { const auto comp0 = fmt::format("{}_1(&{}, {} + 0u, {}u, {})", fetchFn, buf, offs, mapping.frac, le); const auto comp1 = fmt::format("{}_1(&{}, {} + 2u, {}u, {})", fetchFn, buf, offs, mapping.frac, le); const auto comp2 = fmt::format("{}_1(&{}, {} + 4u, {}u, {})", fetchFn, buf, offs, mapping.frac, le); return fmt::format("vec3f({}, {}, {})", comp0, comp1, comp2); } return fmt::format("{}_{}(&{}, {}, {}u, {})", fetchFn, mapping.cnt, buf, offs, mapping.frac, le); } auto fetch_attr(const AttrConfig& mapping, std::string_view buf, std::string_view offs, bool le) -> std::string { switch (mapping.compType) { case GX_U8: return fmt::format("fetch_u8_{}(&{}, {}, {}, {})", mapping.cnt, buf, offs, mapping.frac, le); case GX_S8: return fmt::format("fetch_s8_{}(&{}, {}, {}, {})", mapping.cnt, buf, offs, mapping.frac, le); case GX_U16: return fetch_fixed16_attr("fetch_u16"sv, mapping, buf, offs, le); case GX_S16: return fetch_fixed16_attr("fetch_s16"sv, mapping, buf, offs, le); case GX_F32: return fmt::format("fetch_f32_{}(&{}, {}, {})", mapping.cnt, buf, offs, le); case GX_RGBA8: return fmt::format("unpack4x8unorm(load_u32_raw(&{}, {}))", buf, offs); default: Log.fatal("fetch_attr: Unimplemented {}", static_cast(mapping.compType)); } } auto fetch_color_attr(const AttrConfig& mapping, std::string_view buf, std::string_view offs, bool le) -> std::string { switch (mapping.compType) { case GX_RGB565: return fmt::format("fetch_rgb565(&{}, {}, {})", buf, offs, le); case GX_RGB8: return fmt::format("fetch_rgb8(&{}, {}, {})", buf, offs, le); case GX_RGBX8: return fmt::format("fetch_rgbx8(&{}, {}, {})", buf, offs, le); case GX_RGBA4: return fmt::format("fetch_rgba4(&{}, {}, {})", buf, offs, le); case GX_RGBA6: return fmt::format("fetch_rgba6(&{}, {}, {})", buf, offs, le); case GX_RGBA8: return fmt::format("fetch_rgba8(&{}, {}, {})", buf, offs, le); default: Log.fatal("fetch_color_attr: Unimplemented {}", static_cast(mapping.compType)); } } struct AttrAddress { std::string offs; std::string_view buf; bool le; }; auto attr_address(const AttrConfig& mapping, GXAttr attr, std::string_view vidx, u32 vtxStride, u32 dlExtra, u32 within) -> AttrAddress { const u32 dlOffset = mapping.offset + dlExtra; if (mapping.attrType == GX_INDEX8) { return {fmt::format("ubuf.array_start[{}] + raw_fetch_u8_1(&vbuf, ubuf.vtx_start + {} * {}u + {}u) * {}u + {}u", attr - GX_VA_POS, vidx, vtxStride, dlOffset, mapping.stride, within), "abuf"sv, mapping.le}; } if (mapping.attrType == GX_INDEX16) { return { fmt::format("ubuf.array_start[{}] + raw_fetch_u16_1(&vbuf, ubuf.vtx_start + {} * {}u + {}u, false) * {}u + {}u", attr - GX_VA_POS, vidx, vtxStride, dlOffset, mapping.stride, within), "abuf"sv, mapping.le}; } return {fmt::format("ubuf.vtx_start + {} * {}u + {}u", vidx, vtxStride, dlOffset + within), "vbuf"sv, false}; } auto attr_load(const ShaderConfig& config, GXAttr attr, std::string_view vidx) -> std::string { const auto& mapping = config.attrs[attr]; if (mapping.attrType == GX_NONE) { return vtx_attr(config, attr); } const auto [offs, buf, le] = attr_address(mapping, attr, vidx, config.vtxStride, 0u, 0u); switch (attr) { case GX_VA_PNMTXIDX: return fmt::format("(raw_fetch_u8_1(&{}, {}) / 3u)", buf, offs); case GX_VA_TEX0MTXIDX: case GX_VA_TEX1MTXIDX: case GX_VA_TEX2MTXIDX: case GX_VA_TEX3MTXIDX: case GX_VA_TEX4MTXIDX: case GX_VA_TEX5MTXIDX: case GX_VA_TEX6MTXIDX: case GX_VA_TEX7MTXIDX: return fmt::format("raw_fetch_u8_1(&{}, {})", buf, offs); case GX_VA_POS: { const auto posLoad = fetch_attr(mapping, buf, offs, le); if (mapping.cnt == 2) { return fmt::format("vec3f({}, 0.0)", posLoad); } return posLoad; } case GX_VA_NRM: // NBT: normal only here; binormal/tangent loaded via attr_load_nbt_slice if (mapping.cnt > 3) { auto nrmMapping = mapping; nrmMapping.cnt = 3; return fetch_attr(nrmMapping, buf, offs, le); } return fetch_attr(mapping, buf, offs, le); case GX_VA_CLR0: case GX_VA_CLR1: return fetch_color_attr(mapping, buf, offs, le); case GX_VA_TEX0: case GX_VA_TEX1: case GX_VA_TEX2: case GX_VA_TEX3: case GX_VA_TEX4: case GX_VA_TEX5: case GX_VA_TEX6: case GX_VA_TEX7: { const auto texLoad = fetch_attr(mapping, buf, offs, le); if (mapping.cnt == 1) { return fmt::format("vec2f({}, 0.0)", texLoad); } return texLoad; } default: Log.fatal("attr_load: Unimplemented {}", attr); } } enum class NbtSlice : u8 { N, B, T, }; auto attr_load_nbt_slice(const ShaderConfig& config, NbtSlice slice, std::string_view vidx) -> std::string { const auto& mapping = config.attrs[GX_VA_NRM]; if (mapping.attrType == GX_NONE || mapping.cnt != 9) { Log.fatal("attr_load_nbt_slice: GX_TG_BINRM/TANGENT requires GX_NRM_NBT or GX_NRM_NBT3"); } const auto sliceIdx = static_cast(slice); const auto compsize = comp_type_size(GX_VA_NRM, static_cast(mapping.compType)); u32 dlExtra = 0; if (mapping.nbt3) { if (mapping.attrType == GX_INDEX8) { dlExtra = sliceIdx; } else if (mapping.attrType == GX_INDEX16) { dlExtra = sliceIdx * 2u; } } const u32 within = sliceIdx * 3u * compsize; const auto [offs, buf, le] = attr_address(mapping, GX_VA_NRM, vidx, config.vtxStride, dlExtra, within); auto sliceMapping = mapping; sliceMapping.cnt = 3; return fetch_attr(sliceMapping, buf, offs, le); } static constexpr std::string_view nbt_slice_local(NbtSlice slice) noexcept { return slice == NbtSlice::B ? "in_binrm" : "in_tangent"; } static constexpr bool is_emboss_texgen(GXTexGenType type) noexcept { return type >= GX_TG_BUMP0 && type <= GX_TG_BUMP7; } auto lighting_func(const ShaderConfig& config, const ColorChannelConfig& cc, u8 i, bool alpha) -> std::string { std::string_view swizzle = alpha ? ".a"sv : ""sv; std::string outVar; std::string_view posVar; if (UsePerPixelLighting) { outVar = fmt::format("rast{}", i); posVar = "in.mv_pos"sv; } else { outVar = fmt::format("out.cc{}", i); posVar = "mv_pos"sv; } std::string ambSrc, matSrc; if (cc.ambSrc == GX_SRC_VTX) { if (UsePerPixelLighting) { ambSrc = fmt::format("in.clr{}", i); } else { ambSrc = vtx_attr(config, static_cast(GX_VA_CLR0 + i)); } } else if (cc.ambSrc == GX_SRC_REG) { ambSrc = fmt::format("ubuf.cc{0}{1}_amb", i, alpha ? "a"sv : ""sv); } if (cc.matSrc == GX_SRC_VTX) { if (UsePerPixelLighting) { matSrc = fmt::format("in.clr{}", i); } else { matSrc = vtx_attr(config, static_cast(GX_VA_CLR0 + i)); } } else if (cc.matSrc == GX_SRC_REG) { matSrc = fmt::format("ubuf.cc{0}{1}_mat", i, alpha ? "a"sv : ""sv); } if (!cc.lightingEnabled) { return fmt::format("\n {0}{2} = {1}{2};", outVar, matSrc, swizzle); } GXDiffuseFn diffFn = cc.diffFn; std::string lightAttnFn; if (cc.attnFn == GX_AF_NONE) { lightAttnFn = "attn = 1.0;"s; } else if (cc.attnFn == GX_AF_SPOT) { lightAttnFn = fmt::format(R"""( var cosine = max(0.0, dot(ldir, light.dir)); var cos_attn = dot(light.cos_att, vec3f(1.0, cosine, cosine * cosine)); var dist_attn = dot(light.dist_att, vec3f(1.0, dist, dist2)); attn = max(0.0, cos_attn / dist_attn);)"""); } else if (cc.attnFn == GX_AF_SPEC) { std::string_view normal = UsePerPixelLighting ? "in.mv_nrm"sv : "mv_nrm"sv; std::string dist_attn = diffFn != GX_DF_NONE ? "max(0.0, dot(normalize(light.dist_att), vec3f(1.0, attn, attn * attn)));" : "max(0.0, dot(light.dist_att, vec3f(1.0, attn, attn * attn)));"; lightAttnFn = fmt::format(R"""( attn = select(0.0, max(0.0, dot({0}, light.dir)), dot({0}, ldir) >= 0.0); var cos_attn = dot(light.cos_att, vec3f(1.0, attn, attn * attn)); var dist_attn = {1}; attn = max(0.0, cos_attn / dist_attn);)""", normal, dist_attn); } std::string_view lightDiffFn; if (diffFn == GX_DF_NONE) { lightDiffFn = "1.0"sv; } else if (diffFn == GX_DF_SIGN) { if (UsePerPixelLighting) { lightDiffFn = "dot(ldir, in.mv_nrm)"sv; } else { lightDiffFn = "dot(ldir, mv_nrm)"sv; } } else if (diffFn == GX_DF_CLAMP) { if (UsePerPixelLighting) { lightDiffFn = "max(0.0, dot(ldir, in.mv_nrm))"sv; } else { lightDiffFn = "max(0.0, dot(ldir, mv_nrm))"sv; } } return fmt::format(R"""( {{ var lighting = {5}; for (var i = 0u; i < {1}u; i++) {{ if ((ubuf.lightState{0}{9} & (1u << i)) == 0u) {{ continue; }} var light = ubuf.lights[i]; var ldir = light.pos - {6}; var dist2 = dot(ldir, ldir); var dist = sqrt(dist2); ldir = ldir / dist; var attn: f32;{2} var diff = {3}; lighting = lighting + (attn * diff * light.color); }} {7}{8} = ({4} * clamp(lighting, vec4f(0.0), vec4f(1.0))){8}; }})""", i, GX::MaxLights, lightAttnFn, lightDiffFn, matSrc, ambSrc, posVar, outVar, swizzle, alpha ? "a"sv : ""sv); } std::string build_shader_source(const ShaderConfig& config) noexcept { ZoneScoped; const auto hash = xxh3_hash(config); const auto info = build_shader_info(config); if (EnableDebugPrints && !g_seenShaders.contains(hash)) { g_seenShaders.insert(hash); Log.info("Shader config (hash {:x}):", hash); { for (int i = 0; i < config.tevStageCount; ++i) { const auto& stage = config.tevStages[i]; Log.info(" tevStages[{}]:", i); Log.info(" color_a: {}", TevColorArgNames[stage.colorPass.a]); Log.info(" color_b: {}", TevColorArgNames[stage.colorPass.b]); Log.info(" color_c: {}", TevColorArgNames[stage.colorPass.c]); Log.info(" color_d: {}", TevColorArgNames[stage.colorPass.d]); Log.info(" alpha_a: {}", TevAlphaArgNames[stage.alphaPass.a]); Log.info(" alpha_b: {}", TevAlphaArgNames[stage.alphaPass.b]); Log.info(" alpha_c: {}", TevAlphaArgNames[stage.alphaPass.c]); Log.info(" alpha_d: {}", TevAlphaArgNames[stage.alphaPass.d]); Log.info(" color_op_clamp: {}", stage.colorOp.clamp); Log.info(" color_op_op: {}", stage.colorOp.op); Log.info(" color_op_bias: {}", stage.colorOp.bias); Log.info(" color_op_scale: {}", stage.colorOp.scale); Log.info(" color_op_reg_id: {}", stage.colorOp.outReg); Log.info(" alpha_op_clamp: {}", stage.alphaOp.clamp); Log.info(" alpha_op_op: {}", stage.alphaOp.op); Log.info(" alpha_op_bias: {}", stage.alphaOp.bias); Log.info(" alpha_op_scale: {}", stage.alphaOp.scale); Log.info(" alpha_op_reg_id: {}", stage.alphaOp.outReg); Log.info(" kc_sel: {}", stage.kcSel); Log.info(" ka_sel: {}", stage.kaSel); Log.info(" texCoordId: {}", stage.texCoordId); Log.info(" texMapId: {}", stage.texMapId); Log.info(" channelId: {}", stage.channelId); Log.info(" tevSwapRas: {}", stage.tevSwapRas); Log.info(" tevSwapTex: {}", stage.tevSwapTex); Log.info(" indTexStage: {}", stage.indTexStage); Log.info(" indTexFormat: {}", stage.indTexFormat); Log.info(" indTexBiasSel: {}", stage.indTexBiasSel); Log.info(" indTexAlphaSel: {}", stage.indTexAlphaSel); Log.info(" indTexMtxId: {}", stage.indTexMtxId); Log.info(" indTexWrapS: {}", stage.indTexWrapS); Log.info(" indTexWrapT: {}", stage.indTexWrapT); Log.info(" indTexUseOrigLOD: {}", stage.indTexUseOrigLOD); Log.info(" indTexAddPrev: {}", stage.indTexAddPrev); } Log.info(" numIndStages: {}", config.numIndStages); for (u32 i = 0; i < config.numIndStages; ++i) { const auto& stage = config.indStages[i]; Log.info(" indStages[{}]: texCoordId {} texMapId {} scaleS {} scaleT {}", i, stage.texCoordId, stage.texMapId, stage.scaleS, stage.scaleT); } for (int i = 0; i < config.colorChannels.size(); ++i) { const auto& chan = config.colorChannels[i]; Log.info(" colorChannels[{}]: enabled {} mat {} amb {}", static_cast(i), chan.lightingEnabled, chan.matSrc, chan.ambSrc); } for (int i = 0; i < config.tcgs.size(); ++i) { const auto& tcg = config.tcgs[i]; if (tcg.src != GX_MAX_TEXGENSRC) { Log.info(" tcg[{}]: src {} mtx {} post {} type {} norm {}", i, tcg.src, tcg.mtx, tcg.postMtx, tcg.type, tcg.normalize); } } Log.info(" alphaCompare: comp0 {} ref0 {} op {} comp1 {} ref1 {}", config.alphaCompare.comp0, config.alphaCompare.ref0, config.alphaCompare.op, config.alphaCompare.comp1, config.alphaCompare.ref1); Log.info(" fogType: {}", config.fogType); } } std::string uniformPre; std::string uniBufAttrs; std::string texBindings; std::string vtxOutAttrs; std::string vtxInAttrs; std::string vtxXfrAttrsPre; std::string vtxXfrAttrs; size_t vtxOutIdx = 0; // Load points for line/point expansion std::string_view vidxAttr = "vidx"sv; if (config.lineMode != 0) { vtxInAttrs += ",\n @builtin(instance_index) iidx: u32"; uniBufAttrs += "\n line_width: f32," "\n line_aspect_y: f32," "\n line_tex_offset: f32," "\n line_texcoord_mask: u32,"; if (config.lineMode == 3) { // GX_POINTS: each instance = one vertex, expand to quad vtxXfrAttrsPre += fmt::format( "\n let in_vidx = iidx;" "\n let in_pos = {};" "\n let in_pnmtxidx = {};" "\n let mv_pos = vec4f(in_pos, 1.0) * ubuf.postex_mtx[in_pnmtxidx];", attr_load(config, GX_VA_POS, "in_vidx"sv), attr_load(config, GX_VA_PNMTXIDX, "in_vidx"sv)); } else { // GX_LINES / GX_LINESTRIP: each instance = two vertices, expand to quad vtxXfrAttrsPre += fmt::format( "\n let use_b = vidx >= 2u;" "\n let vidx_a = iidx * {}u;" "\n let vidx_b = vidx_a + 1u;" "\n let in_vidx = select(vidx_a, vidx_b, use_b);" "\n let pos_a = {};" "\n let pos_b = {};" "\n let in_pos = select(pos_a, pos_b, use_b);" "\n let pnmtxidx_a = {};" "\n let pnmtxidx_b = {};" "\n let in_pnmtxidx = select(pnmtxidx_a, pnmtxidx_b, use_b);" "\n let mv_pos_a = vec4f(pos_a, 1.0) * ubuf.postex_mtx[pnmtxidx_a];" "\n let mv_pos_b = vec4f(pos_b, 1.0) * ubuf.postex_mtx[pnmtxidx_b];" "\n let mv_pos = select(mv_pos_a, mv_pos_b, use_b);", config.lineMode == 1 ? 2 : 1, attr_load(config, GX_VA_POS, "vidx_a"sv), attr_load(config, GX_VA_POS, "vidx_b"sv), attr_load(config, GX_VA_PNMTXIDX, "vidx_a"sv), attr_load(config, GX_VA_PNMTXIDX, "vidx_b"sv)); } vidxAttr = "in_vidx"sv; } else if (config.attrs[GX_VA_PNMTXIDX].attrType == GX_NONE) { vtxXfrAttrsPre += "\n let in_pnmtxidx = ubuf.current_pnmtx;"; } // Load vertex attributes for (GXAttr attr = GX_VA_PNMTXIDX; attr <= GX_VA_TEX7; attr = static_cast(attr + 1)) { const auto attrType = config.attrs[attr].attrType; if (attrType == GX_NONE) { continue; } // in_pnmtxidx and in_pos written above for line mode if ((attr != GX_VA_PNMTXIDX && attr != GX_VA_POS) || config.lineMode == 0) { vtxXfrAttrsPre += fmt::format("\n let {} = {};", vtx_attr(config, attr), attr_load(config, attr, vidxAttr)); } } bool needsBinrm = false; bool needsTangent = false; for (int i = 0; i < info.sampledTexCoords.size(); ++i) { if (!info.sampledTexCoords.test(i)) { continue; } const bool emboss = is_emboss_texgen(config.tcgs[i].type); needsBinrm = needsBinrm || config.tcgs[i].src == GX_TG_BINRM || emboss; needsTangent = needsTangent || config.tcgs[i].src == GX_TG_TANGENT || emboss; } if (needsBinrm) { vtxXfrAttrsPre += fmt::format("\n let {} = {};", nbt_slice_local(NbtSlice::B), attr_load_nbt_slice(config, NbtSlice::B, vidxAttr)); } if (needsTangent) { vtxXfrAttrsPre += fmt::format("\n let {} = {};", nbt_slice_local(NbtSlice::T), attr_load_nbt_slice(config, NbtSlice::T, vidxAttr)); } if (config.lineMode == 0) { vtxXfrAttrsPre += fmt::format( "\n let mv_pos = vec4f({}, 1.0) * ubuf.postex_mtx[in_pnmtxidx];" "\n out.pos = vec4f(mv_pos, 1.0) * ubuf.proj;", vtx_attr(config, GX_VA_POS)); } else if (config.lineMode == 3) { // GX_POINTS: expand single vertex to axis-aligned screen-space square vtxXfrAttrsPre += "\n let clip = vec4f(mv_pos, 1.0) * ubuf.proj;" "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" "\n let point_size = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" "\n let x_sign = select(-1.0, 1.0, (vidx & 1u) != 0u);" "\n let y_sign = select(-1.0, 1.0, vidx >= 2u);" "\n let offset_px = vec2f(x_sign, y_sign) * (point_size / 2.0);" "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" "\n out.pos = vec4f(clip.xy + offset_ndc * clip.w, clip.zw);"; } else { // GX_LINES / GX_LINESTRIP: expand line segment perpendicular to direction vtxXfrAttrsPre += "\n let clip_a = vec4f(mv_pos_a, 1.0) * ubuf.proj;" "\n let clip_b = vec4f(mv_pos_b, 1.0) * ubuf.proj;" "\n let ndc_a = clip_a.xy / clip_a.w;" "\n let ndc_b = clip_b.xy / clip_b.w;" "\n let viewport_scale = ubuf.render_viewport_size / max(ubuf.logical_viewport_size, vec2f(1.0));" "\n let delta_px = (ndc_b - ndc_a) / 2.0 * ubuf.render_viewport_size;" "\n let dir_px = select(vec2f(1.0, 0.0), normalize(delta_px), dot(delta_px, delta_px) > 1e-10);" "\n let perp_px = vec2f(-dir_px.y, dir_px.x);" "\n let line_width = ubuf.line_width * min(viewport_scale.x, viewport_scale.y);" "\n let offset_px = perp_px * (line_width / 2.0) * select(-1.0, 1.0, (vidx & 1u) != 0u);" "\n let offset_ndc = (offset_px * 2.0) / ubuf.render_viewport_size;" "\n let clip_base = select(clip_a, clip_b, use_b);" "\n out.pos = vec4f(clip_base.xy + offset_ndc * clip_base.w, clip_base.zw);"; } if constexpr (UseReversedZ) { vtxXfrAttrsPre += "\n out.pos.z = -out.pos.z;"; } else { vtxXfrAttrsPre += "\n out.pos.z += out.pos.w;"; } vtxXfrAttrsPre += fmt::format( "\n let nrm_tmp = vec4f({}, 0.0) * ubuf.nrm_mtx[in_pnmtxidx];" "\n let mv_nrm = select(nrm_tmp, normalize(nrm_tmp), dot(nrm_tmp, nrm_tmp) > 1e-10);", vtx_attr(config, GX_VA_NRM)); if constexpr (EnableNormalVisualization) { vtxOutAttrs += fmt::format("\n @location({}) nrm: vec3f,", vtxOutIdx++); vtxXfrAttrsPre += "\n out.nrm = mv_nrm;"; } uniBufAttrs += "\n proj: mat4x4f,"; uniBufAttrs += fmt::format("\n postex_mtx: array,", MaxPnMtx + MaxTexMtx); uniBufAttrs += fmt::format("\n nrm_mtx: array,", MaxPnMtx); std::string fragmentFnPre; std::string fragmentFn; static std::array regName{"prev"sv, "tevreg0"sv, "tevreg1"sv, "tevreg2"sv}; for (u32 idx = 0; idx < config.tevStageCount; ++idx) { const auto& stage = config.tevStages[idx]; { std::string_view outReg = regName[stage.colorOp.outReg]; std::string op = tev_color_op( stage.colorOp.op, tev_bias(stage.colorOp.bias), tev_scale(stage.colorOp.scale), stage.colorOp.clamp, color_arg_reg(stage.colorPass.a, idx, config, stage), color_arg_reg(stage.colorPass.b, idx, config, stage), color_arg_reg(stage.colorPass.c, idx, config, stage), color_arg_reg(stage.colorPass.d, idx, config, stage)); fragmentFn += fmt::format("\n // TEV stage {2}\n {0} = vec4f({1}, {0}.a);", outReg, op, idx); } { std::string_view outReg = regName[stage.alphaOp.outReg]; std::string op = tev_alpha_op( stage.alphaOp.op, tev_bias(stage.alphaOp.bias), tev_scale(stage.alphaOp.scale), stage.alphaOp.clamp, alpha_arg_reg(stage.alphaPass.a, idx, config, stage), alpha_arg_reg(stage.alphaPass.b, idx, config, stage), alpha_arg_reg(stage.alphaPass.c, idx, config, stage), alpha_arg_reg(stage.alphaPass.d, idx, config, stage)); fragmentFn += fmt::format("\n {0}.a = {1};", outReg, op); } } { const auto& lastStage = config.tevStages[config.tevStageCount - 1]; if (lastStage.colorOp.outReg != 0) { fragmentFn += fmt::format("\n prev = vec4f({0}.rgb, prev.a);", regName[lastStage.colorOp.outReg]); } if (lastStage.alphaOp.outReg != 0) { fragmentFn += fmt::format("\n prev.a = {0}.a;", regName[lastStage.alphaOp.outReg]); } } if (info.loadsTevReg.test(0)) { uniBufAttrs += "\n tevprev: vec4f,"; fragmentFnPre += "\n var prev = ubuf.tevprev;"; } else { fragmentFnPre += "\n var prev: vec4f;"; } for (int i = 1 /* Skip TEVPREV */; i < info.loadsTevReg.size(); ++i) { if (info.loadsTevReg.test(i)) { uniBufAttrs += fmt::format("\n tevreg{}: vec4f,", i - 1); fragmentFnPre += fmt::format("\n var tevreg{0} = ubuf.tevreg{0};", i - 1); } else if (info.writesTevReg.test(i)) { fragmentFnPre += fmt::format("\n var tevreg{0}: vec4f;", i - 1); } } if (info.lightingEnabled) { uniBufAttrs += fmt::format(FMT_STRING(R"""( lights: array, lightState0: u32, lightState1: u32, lightState0a: u32, lightState1a: u32,)"""), GX::MaxLights); uniformPre += "\n" "struct Light {\n" " pos: vec3f,\n" " dir: vec3f,\n" " color: vec4f,\n" " cos_att: vec3f,\n" " dist_att: vec3f,\n" "};"; if (UsePerPixelLighting) { vtxOutAttrs += fmt::format("\n @location({}) mv_pos: vec3f,", vtxOutIdx++); vtxOutAttrs += fmt::format("\n @location({}) mv_nrm: vec3f,", vtxOutIdx++); vtxXfrAttrs += fmt::format(FMT_STRING(R"""( out.mv_pos = mv_pos; out.mv_nrm = mv_nrm;)""")); } } for (int i = 0; i < info.sampledColorChannels.size(); ++i) { if (!info.sampledColorChannels.test(i)) { continue; } const auto& cc = config.colorChannels[i]; const auto& cca = config.colorChannels[i + GX_ALPHA0]; if (cc.lightingEnabled && cc.ambSrc == GX_SRC_REG) { uniBufAttrs += fmt::format("\n cc{0}_amb: vec4f,", i); } if (cc.matSrc == GX_SRC_REG) { uniBufAttrs += fmt::format("\n cc{0}_mat: vec4f,", i); } if (cca.lightingEnabled && cca.ambSrc == GX_SRC_REG) { uniBufAttrs += fmt::format("\n cc{0}a_amb: vec4f,", i); } if (cca.matSrc == GX_SRC_REG) { uniBufAttrs += fmt::format("\n cc{0}a_mat: vec4f,", i); } // Output vertex color if necessary if (UsePerPixelLighting) { if ((cc.lightingEnabled && cc.ambSrc == GX_SRC_VTX) || cc.matSrc == GX_SRC_VTX || (cca.lightingEnabled && cca.ambSrc == GX_SRC_VTX) || cca.matSrc == GX_SRC_VTX) { vtxOutAttrs += fmt::format("\n @location({}) clr{}: vec4f,", vtxOutIdx++, i); vtxXfrAttrs += fmt::format("\n out.clr{} = {};", i, vtx_attr(config, static_cast(GX_VA_CLR0 + i))); } } if (UsePerPixelLighting) { fragmentFnPre += fmt::format("\n var rast{}: vec4f;", i); fragmentFnPre += lighting_func(config, cc, i, false); fragmentFnPre += lighting_func(config, cca, i, true); } else { vtxOutAttrs += fmt::format("\n @location({}) cc{}: vec4f,", vtxOutIdx++, i); vtxXfrAttrs += lighting_func(config, cc, i, false); vtxXfrAttrs += lighting_func(config, cca, i, true); fragmentFnPre += fmt::format("\n var rast{0} = in.cc{0};", i); } } for (int i = 0; i < info.sampledKColors.size(); ++i) { if (info.sampledKColors.test(i)) { uniBufAttrs += fmt::format("\n kcolor{}: vec4f,", i); } } for (int i = 0; i < info.sampledTexCoords.size(); ++i) { if (!info.sampledTexCoords.test(i)) { continue; } const auto& tcg = config.tcgs[i]; if (tcg.type == GX_TG_MTX3x4) { vtxOutAttrs += fmt::format("\n @location({}) tex{}_uvw: vec3f,", vtxOutIdx++, i); } else { vtxOutAttrs += fmt::format("\n @location({}) tex{}_uv: vec2f,", vtxOutIdx++, i); } if (is_emboss_texgen(tcg.type)) { // Emboss bump: offset the source texcoord by the light projected onto tangent/binormal const u32 lightIdx = tcg.type - GX_TG_BUMP0; vtxXfrAttrs += fmt::format( "\n let bump_ldir{0} = normalize(ubuf.lights[{1}].pos - mv_pos);" "\n let bump_tan{0} = vec4f(in_tangent, 0.0) * ubuf.nrm_mtx[in_pnmtxidx];" "\n let bump_bin{0} = vec4f(in_binrm, 0.0) * ubuf.nrm_mtx[in_pnmtxidx];" "\n out.tex{0}_uv = tc{2}_proj.xy + vec2f(dot(bump_ldir{0}, bump_tan{0}), dot(bump_ldir{0}, " "bump_bin{0}));", i, lightIdx, tcg.embossSrc); fragmentFnPre += fmt::format("\n var tex{0}_uv = in.tex{0}_uv.xy;", i); continue; } if (tcg.src >= GX_TG_TEX0 && tcg.src <= GX_TG_TEX7) { vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0, 1.0);", i, vtx_attr(config, GXAttr(GX_VA_TEX0 + (tcg.src - GX_TG_TEX0)))); } else if (tcg.src == GX_TG_POS) { vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, vtx_attr(config, GX_VA_POS)); } else if (tcg.src == GX_TG_NRM) { vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, vtx_attr(config, GX_VA_NRM)); } else if (tcg.src == GX_TG_COLOR0) { vtxXfrAttrs += fmt::format("\n var tc{} = {};", i, vtx_attr(config, GX_VA_CLR0)); } else if (tcg.src == GX_TG_COLOR1) { vtxXfrAttrs += fmt::format("\n var tc{} = {};", i, vtx_attr(config, GX_VA_CLR1)); } else if (tcg.src == GX_TG_BINRM) { vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, nbt_slice_local(NbtSlice::B)); } else if (tcg.src == GX_TG_TANGENT) { vtxXfrAttrs += fmt::format("\n var tc{} = vec4f({}, 1.0);", i, nbt_slice_local(NbtSlice::T)); } else UNLIKELY FATAL("unhandled tcg src {}", underlying(tcg.src)); if (tcg.type == GX_TG_MTX2x4 || tcg.type == GX_TG_MTX3x4) { if (info.indexAttr.test(GX_VA_TEX0MTXIDX + i)) { vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0} * ubuf.postex_mtx[in_texmtxidx{0} / 3u];", i); } else if (tcg.mtx == GX_IDENTITY) { vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0}.xyz;", i); } else { u32 texMtxIdx = (tcg.mtx) / 3; vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = tc{0} * ubuf.postex_mtx[{1}];", i, texMtxIdx); } if (tcg.type == GX_TG_MTX2x4) { vtxXfrAttrs += fmt::format("\n tc{0}_tmp.z = 1.0f;", i); } } else if (tcg.type == GX_TG_SRTG) { vtxXfrAttrs += fmt::format("\n var tc{0}_tmp = vec3f(tc{0}.xy, 1.0f);", i); } if (tcg.normalize) { vtxXfrAttrs += fmt::format("\n tc{0}_tmp = normalize(tc{0}_tmp);", i); } if (tcg.postMtx == GX_PTIDENTITY) { vtxXfrAttrs += fmt::format("\n var tc{0}_proj = tc{0}_tmp;", i); } else { u32 postMtxIdx = (tcg.postMtx - GX_PTTEXMTX0) / 3; vtxXfrAttrs += fmt::format("\n var tc{0}_proj = vec4f(tc{0}_tmp.xyz, 1.0) * ubuf.postmtx[{1}];", i, postMtxIdx); } // Apply line/point tex offset if (config.lineMode == 3) { // GX_POINTS: offset S for right columns, T for bottom rows vtxXfrAttrs += fmt::format( "\n if ((ubuf.line_texcoord_mask & (1u << {0})) != 0u) {{" "\n if ((vidx & 1u) != 0u) {{ tc{0}_proj.x += ubuf.line_tex_offset; }}" "\n if (vidx >= 2u) {{ tc{0}_proj.y += ubuf.line_tex_offset; }}" "\n }}", i); } else if (config.lineMode != 0) { // GX_LINES / GX_LINESTRIP: offset one axis for perpendicular side vtxXfrAttrs += fmt::format( "\n if ((ubuf.line_texcoord_mask & (1u << {0})) != 0u && (vidx & 1u) != 0u) {{" "\n tc{0}_proj.y += ubuf.line_tex_offset;" "\n }}", i); } if (tcg.type == GX_TG_MTX3x4) { vtxXfrAttrs += fmt::format("\n out.tex{0}_uvw = tc{0}_proj.xyz;", i); fragmentFnPre += fmt::format("\n var tex{0}_uv = in.tex{0}_uvw.xy / in.tex{0}_uvw.z;", i); } else { vtxXfrAttrs += fmt::format("\n out.tex{0}_uv = tc{0}_proj.xy;", i); fragmentFnPre += fmt::format("\n var tex{0}_uv = in.tex{0}_uv.xy;", i); } } // Multiple TEV stages may reference the same indirect stage, // so we sample each indirect texture only once. const auto ind_scale = [](const GXIndTexScale s) -> std::string_view { switch (s) { case GX_ITS_1: return "1.0"sv; case GX_ITS_2: return "(1.0 / 2.0)"sv; case GX_ITS_4: return "(1.0 / 4.0)"sv; case GX_ITS_8: return "(1.0 / 8.0)"sv; case GX_ITS_16: return "(1.0 / 16.0)"sv; case GX_ITS_32: return "(1.0 / 32.0)"sv; case GX_ITS_64: return "(1.0 / 64.0)"sv; case GX_ITS_128: return "(1.0 / 128.0)"sv; case GX_ITS_256: return "(1.0 / 256.0)"sv; default: FATAL("unhandled indirect scale {}", underlying(s)); } }; for (int i = 0; i < info.usedIndStages.size(); ++i) { if (!info.usedIndStages.test(i)) { continue; } const auto& indStage = config.indStages[i]; const u32 texCoordId = underlying(indStage.texCoordId); const u32 texMapId = underlying(indStage.texMapId); // GX applies the SU texture-coordinate scale before the indirect stage scale. // The shader carries normalized UVs, so convert that texel-space result back // into normalized coordinates for the indirect texture sample. const auto scaleExpr = fmt::format("tex{0}_uv * ubuf.texcoord_scale[{0}].xy * vec2f({1}, {2}) / ubuf.tex{3}_size_bias.xy", texCoordId, ind_scale(indStage.scaleS), ind_scale(indStage.scaleT), texMapId); fragmentFnPre += fmt::format( "\n // Indirect stage {0}" "\n var t_IndTexCoord{0} = 255.0 * textureSampleBias(tex{1}, tex{1}_samp, {2}, " "ubuf.tex{1}_size_bias.z).abg;", i, texMapId, scaleExpr); } if (info.usedIndStages.any()) { fragmentFnPre += "\n var t_TexCoord = vec2f(0.0);"; } for (int i = 0; i < config.tevStageCount; ++i) { const auto& stage = config.tevStages[i]; const bool needsIndirectCoord = stage.indTexMtxId != GX_ITM_OFF; const bool hasIndirectStage = stage.indTexStage < config.numIndStages; const bool needsTevTexCoord = needsIndirectCoord || stage.indTexWrapS != GX_ITW_OFF || stage.indTexWrapT != GX_ITW_OFF || stage.indTexAddPrev; const bool needsTextureSample = uses_texture_sample(stage); if (!needsTevTexCoord && !needsTextureSample) { continue; } const bool hasBaseTexCoord = stage.texCoordId != GX_TEXCOORD_NULL; const bool hasBaseTexture = stage.texMapId != GX_TEXMAP_NULL; const bool hasBaseCoord = hasBaseTexCoord && hasBaseTexture; std::string uvIn; if (needsTevTexCoord) { fragmentFnPre += fmt::format("\n // TEV stage {} indirect", i); // Apply indirect texture matrix (produces a texel-space offset) std::string indirectOffsetTexel; if (needsIndirectCoord && hasIndirectStage) { std::string_view fmtShift; switch (stage.indTexFormat) { case GX_ITF_8: break; case GX_ITF_5: fmtShift = " / 8.0"sv; break; case GX_ITF_4: fmtShift = " / 16.0"sv; break; case GX_ITF_3: fmtShift = " / 32.0"sv; break; default: FATAL("unhandled indirect format {}", underlying(stage.indTexFormat)); } if (fmtShift.empty()) { fragmentFnPre += fmt::format("\n var ind{0}_coord = t_IndTexCoord{1};", i, underlying(stage.indTexStage)); } else { fragmentFnPre += fmt::format("\n var ind{0}_coord = floor(t_IndTexCoord{1}{2});", i, underlying(stage.indTexStage), fmtShift); } if (stage.indTexBiasSel != GX_ITB_NONE) { auto bias = stage.indTexFormat == GX_ITF_8 ? "-128.0"sv : "1.0"sv; auto biasS = "0.0"sv, biasT = "0.0"sv, biasU = "0.0"sv; if (stage.indTexBiasSel == GX_ITB_S || stage.indTexBiasSel == GX_ITB_ST || stage.indTexBiasSel == GX_ITB_SU || stage.indTexBiasSel == GX_ITB_STU) { biasS = "1.0"sv; } if (stage.indTexBiasSel == GX_ITB_T || stage.indTexBiasSel == GX_ITB_ST || stage.indTexBiasSel == GX_ITB_TU || stage.indTexBiasSel == GX_ITB_STU) { biasT = "1.0"sv; } if (stage.indTexBiasSel == GX_ITB_U || stage.indTexBiasSel == GX_ITB_SU || stage.indTexBiasSel == GX_ITB_TU || stage.indTexBiasSel == GX_ITB_STU) { biasU = "1.0"sv; } fragmentFnPre += fmt::format("\n ind{0}_coord = ind{0}_coord + vec3f({1}, {2}, {3}) * {4};", i, biasS, biasT, biasU, bias); } if (stage.indTexMtxId >= GX_ITM_0 && stage.indTexMtxId <= GX_ITM_2) { // Static 2x3 matrix: dot(mat_row, vec3(S,T,U)) * scale u32 mtxIdx = stage.indTexMtxId - GX_ITM_0; fragmentFnPre += fmt::format( "\n let ind{0}_c0 = ubuf.ind_mtx[{1}][0];" "\n let ind{0}_c1 = ubuf.ind_mtx[{1}][1];", i, mtxIdx); indirectOffsetTexel = fmt::format( "vec2f(" "dot(vec3f(ind{0}_c0.xz, ind{0}_c1.x), ind{0}_coord), " "dot(vec3f(ind{0}_c0.yw, ind{0}_c1.y), ind{0}_coord)" ") * ind{0}_c1.z", i); } else if (stage.indTexMtxId >= GX_ITM_S0 && stage.indTexMtxId <= GX_ITM_S2 && hasBaseCoord) { // Dynamic S: result = scaled texcoord * ind_coord.x * scale / 256 u32 mtxIdx = stage.indTexMtxId - GX_ITM_S0; u32 regTexCoord = underlying(stage.texCoordId); indirectOffsetTexel = fmt::format( "tex{1}_uv * ubuf.texcoord_scale[{1}].xy * ind{0}_coord.x" " * ubuf.ind_mtx[{2}][1][2] / 256.0", i, regTexCoord, mtxIdx); } else if (stage.indTexMtxId >= GX_ITM_T0 && stage.indTexMtxId <= GX_ITM_T2 && hasBaseCoord) { // Dynamic T: result = scaled texcoord * ind_coord.y * scale / 256 u32 mtxIdx = stage.indTexMtxId - GX_ITM_T0; u32 regTexCoord = underlying(stage.texCoordId); indirectOffsetTexel = fmt::format( "tex{1}_uv * ubuf.texcoord_scale[{1}].xy * ind{0}_coord.y" " * ubuf.ind_mtx[{2}][1][2] / 256.0", i, regTexCoord, mtxIdx); } } // Don't convert to/from texel space if we can avoid it const bool useSimpleCoords = stage.indTexMtxId == GX_ITM_OFF && !stage.indTexAddPrev; // Wrap base coord and combine with the indirect translation. auto wrap_comp = [](GXIndTexWrap wrap, std::string&& coord) -> std::string { switch (wrap) { case GX_ITW_OFF: return std::move(coord); case GX_ITW_256: return fmt::format("({} % 256.0)", coord); case GX_ITW_128: return fmt::format("({} % 128.0)", coord); case GX_ITW_64: return fmt::format("({} % 64.0)", coord); case GX_ITW_32: return fmt::format("({} % 32.0)", coord); case GX_ITW_16: return fmt::format("({} % 16.0)", coord); case GX_ITW_0: return "0.0"; default: FATAL("unhandled indirect wrap {}", underlying(wrap)); } }; std::string baseCoordExpr; if (hasBaseCoord) { u32 texCoordId = underlying(stage.texCoordId); if (useSimpleCoords) { baseCoordExpr = fmt::format("tex{}_uv", texCoordId); } else { fragmentFnPre += fmt::format("\n var ind{0}_texel = tex{1}_uv * ubuf.texcoord_scale[{1}].xy;", i, texCoordId); baseCoordExpr = fmt::format("ind{}_texel", i); } } std::string wrappedExpr = baseCoordExpr; if (!baseCoordExpr.empty() && (stage.indTexWrapS != GX_ITW_OFF || stage.indTexWrapT != GX_ITW_OFF)) { wrappedExpr = fmt::format("vec2f({}, {})", wrap_comp(stage.indTexWrapS, fmt::format("{}.x", baseCoordExpr)), wrap_comp(stage.indTexWrapT, fmt::format("{}.y", baseCoordExpr))); } std::string finalCoord; if (!wrappedExpr.empty() && !indirectOffsetTexel.empty()) { finalCoord = fmt::format("{} + ({})", wrappedExpr, indirectOffsetTexel); } else if (!wrappedExpr.empty()) { finalCoord = wrappedExpr; } else { finalCoord = indirectOffsetTexel; } if (info.usedIndStages.any() && !finalCoord.empty()) { if (stage.indTexAddPrev) { fragmentFnPre += fmt::format("\n t_TexCoord += {};", finalCoord); } else { fragmentFnPre += fmt::format("\n t_TexCoord = {};", finalCoord); } if (needsTextureSample && hasBaseTexture) { u32 texMapId = underlying(stage.texMapId); if (useSimpleCoords) { fragmentFnPre += fmt::format("\n var ind{0}_uv = t_TexCoord;", i); } else { fragmentFnPre += fmt::format("\n var ind{0}_uv = t_TexCoord / ubuf.tex{1}_size_bias.xy;", i, texMapId); } uvIn = fmt::format("ind{0}_uv", i); } } } if (!needsTextureSample) { continue; } CHECK(stage.texMapId != GX_TEXMAP_NULL, "unmapped texture for stage {}", i); CHECK(stage.texCoordId != GX_TEXCOORD_NULL, "unmapped texcoord for stage {}", i); if (uvIn.empty()) { // No indirect texturing uvIn = fmt::format("tex{0}_uv", underlying(stage.texCoordId)); } fragmentFnPre += fmt::format("\n var sampled{0} = textureSampleBias(tex{1}, tex{1}_samp, {2}, ubuf.tex{1}_size_bias.z);", i, underlying(stage.texMapId), uvIn); } if (info.usesPTTexMtx.any()) uniBufAttrs += fmt::format("\n postmtx: array,", MaxPTTexMtx); if (info.usesFog) { uniformPre += "\n" "struct Fog {\n" " color: vec4f,\n" " a: f32,\n" " b: f32,\n" " c: f32,\n" " pad: f32,\n" "}"; uniBufAttrs += "\n fog: Fog,"; fragmentFn += fmt::format("\n // Fog\n var fogF = clamp((ubuf.fog.a / (ubuf.fog.b - {})) - ubuf.fog.c, 0.0, 1.0);", UseReversedZ ? "(1.0 - in.pos.z)" : "in.pos.z"); switch (config.fogType) { DEFAULT_FATAL("invalid fog type {}", config.fogType); case GX_FOG_PERSP_LIN: case GX_FOG_ORTHO_LIN: fragmentFn += "\n var fogZ = fogF;"; break; case GX_FOG_PERSP_EXP: case GX_FOG_ORTHO_EXP: fragmentFn += "\n var fogZ = 1.0 - exp2(-8.0 * fogF);"; break; case GX_FOG_PERSP_EXP2: case GX_FOG_ORTHO_EXP2: fragmentFn += "\n var fogZ = 1.0 - exp2(-8.0 * fogF * fogF);"; break; case GX_FOG_PERSP_REVEXP: case GX_FOG_ORTHO_REVEXP: fragmentFn += "\n var fogZ = exp2(-8.0 * (1.0 - fogF));"; break; case GX_FOG_PERSP_REVEXP2: case GX_FOG_ORTHO_REVEXP2: fragmentFn += "\n fogF = 1.0 - fogF;" "\n var fogZ = exp2(-8.0 * fogF * fogF);"; break; } fragmentFn += "\n prev = vec4f(mix(prev.rgb, ubuf.fog.color.rgb, clamp(fogZ, 0.0, 1.0)), prev.a);"; } uniBufAttrs += fmt::format("\n texcoord_scale: array,", MaxTexCoord); if (info.usedIndTexMtxs.any()) { uniBufAttrs += "\n ind_mtx: array,"; } for (int i = 0; i < info.sampledTextures.size(); ++i) { if (!info.sampledTextures.test(i)) { continue; } uniBufAttrs += fmt::format("\n tex{}_size_bias: vec4f,", i); texBindings += fmt::format( "\n@group(2) @binding({1})\n" "var tex{0}: texture_2d;\n" "@group(2) @binding({2})\n" "var tex{0}_samp: sampler;", i, i * 2, i * 2 + 1); } fragmentFn += "\n prev = tev_overflow_vec4f(prev);"; if (config.alphaCompare) { const auto comp0 = alpha_compare(config.alphaCompare.comp0, config.alphaCompare.ref0); const auto comp1 = alpha_compare(config.alphaCompare.comp1, config.alphaCompare.ref1); AlphaCompareExpr pass; switch (config.alphaCompare.op) { DEFAULT_FATAL("invalid alpha compare op {}", underlying(config.alphaCompare.op)); case GX_AOP_AND: pass = alpha_compare_and(comp0, comp1); break; case GX_AOP_OR: pass = alpha_compare_or(comp0, comp1); break; case GX_AOP_XOR: pass = alpha_compare_xor(comp0, comp1); break; case GX_AOP_XNOR: pass = alpha_compare_xnor(comp0, comp1); break; } const auto discard = alpha_compare_not(pass); if (discard.constant == 1) { fragmentFn += "\n // Alpha compare\n discard;"; } else if (discard.constant != 0) { fragmentFn += "\n // Alpha compare" "\n let alphaCompare = u32(round(clamp(prev.a, 0.0, 1.0) * 255.0));"; fragmentFn += fmt::format("\n if ({}) {{ discard; }}", discard.expr); } } if constexpr (EnableNormalVisualization) { fragmentFn += "\n prev = vec4f(in.nrm, prev.a);"; } const auto shaderSource = fmt::format(R"""( fn bswap32(v: u32, le: bool) -> u32 {{ if (le) {{ return v; }} return ((v & 0x000000FFu) << 24u) | ((v & 0x0000FF00u) << 8u) | ((v & 0x00FF0000u) >> 8u) | ((v & 0xFF000000u) >> 24u); }} fn bswap16(v: u32, le: bool) -> u32 {{ return select(((v & 0xFFu) << 8u) | (v >> 8u), v, le); }} fn load_word(p: ptr>, word_idx: u32) -> u32 {{ // This guard is not expected to handle routine out-of-bounds accesses. // It appears to discourage some Adreno drivers/optimizers from storage buffer // optimizations that can cause visual artifacts, including vertex explosions // in Dusklight. if (word_idx < arrayLength(p)) {{ return p[word_idx]; }} return 0u; }} fn load_u8(p: ptr>, byte_off: u32) -> u32 {{ let word = load_word(p, byte_off / 4u); let shift = (byte_off & 3u) * 8u; return (word >> shift) & 0xFFu; }} fn load_u32_raw(p: ptr>, byte_off: u32) -> u32 {{ let word_idx = byte_off >> 2u; let sub = byte_off & 3u; let lo = load_word(p, word_idx); if (sub == 0u) {{ return lo; }} let hi = load_word(p, word_idx + 1u); let shift = sub * 8u; return (lo >> shift) | (hi << (32u - shift)); }} fn load_u16(p: ptr>, byte_off: u32, le: bool) -> u32 {{ let word_idx = byte_off >> 2u; let sub = byte_off & 3u; let word = load_word(p, word_idx); if (sub <= 2u) {{ return bswap16(extractBits(word, sub * 8u, 16u), le); }} let next = load_word(p, word_idx + 1u); let raw = extractBits(word, 24u, 8u) | (extractBits(next, 0u, 8u) << 8u); return bswap16(raw, le); }} fn load_u24(p: ptr>, byte_off: u32, le: bool) -> u32 {{ let raw = load_u32_raw(p, byte_off) & 0x00FFFFFFu; if (le) {{ return raw; }} return ((raw & 0x0000FFu) << 16u) | (raw & 0x00FF00u) | ((raw & 0xFF0000u) >> 16u); }} fn load_u32(p: ptr>, byte_off: u32, le: bool) -> u32 {{ return bswap32(load_u32_raw(p, byte_off), le); }} fn load_f32(p: ptr>, byte_off: u32, le: bool) -> f32 {{ return bitcast(load_u32(p, byte_off, le)); }} fn raw_fetch_u8_1(p: ptr>, byte_off: u32) -> u32 {{ return load_u8(p, byte_off); }} fn raw_fetch_u8_2(p: ptr>, byte_off: u32) -> vec2u {{ let word_idx = byte_off >> 2u; let sub = byte_off & 3u; let word = load_word(p, word_idx); if (sub <= 2u) {{ let shift = sub * 8u; return vec2u( extractBits(word, shift + 0u, 8u), extractBits(word, shift + 8u, 8u), ); }} let next = load_word(p, word_idx + 1u); return vec2u( extractBits(word, 24u, 8u), extractBits(next, 0u, 8u), ); }} fn raw_fetch_u8_3(p: ptr>, byte_off: u32) -> vec3u {{ let raw = load_u32_raw(p, byte_off); return vec3u( extractBits(raw, 0u, 8u), extractBits(raw, 8u, 8u), extractBits(raw, 16u, 8u), ); }} fn raw_fetch_u8_4(p: ptr>, byte_off: u32) -> vec4u {{ let raw = load_u32_raw(p, byte_off); return vec4u( extractBits(raw, 0u, 8u), extractBits(raw, 8u, 8u), extractBits(raw, 16u, 8u), extractBits(raw, 24u, 8u), ); }} fn raw_fetch_u16_1(p: ptr>, byte_off: u32, le: bool) -> u32 {{ return load_u16(p, byte_off, le); }} fn raw_fetch_u16_2(p: ptr>, byte_off: u32, le: bool) -> vec2u {{ return vec2u( load_u16(p, byte_off + 0u, le), load_u16(p, byte_off + 2u, le), ); }} fn raw_fetch_u16_3(p: ptr>, byte_off: u32, le: bool) -> vec3u {{ return vec3u( load_u16(p, byte_off + 0u, le), load_u16(p, byte_off + 2u, le), load_u16(p, byte_off + 4u, le), ); }} fn raw_fetch_u16_4(p: ptr>, byte_off: u32, le: bool) -> vec4u {{ return vec4u( load_u16(p, byte_off + 0u, le), load_u16(p, byte_off + 2u, le), load_u16(p, byte_off + 4u, le), load_u16(p, byte_off + 6u, le), ); }} fn raw_fetch_f32_1(p: ptr>, byte_off: u32, le: bool) -> f32 {{ return load_f32(p, byte_off, le); }} fn raw_fetch_f32_2(p: ptr>, byte_off: u32, le: bool) -> vec2f {{ return vec2f( load_f32(p, byte_off + 0u, le), load_f32(p, byte_off + 4u, le), ); }} fn raw_fetch_f32_3(p: ptr>, byte_off: u32, le: bool) -> vec3f {{ return vec3f( load_f32(p, byte_off + 0u, le), load_f32(p, byte_off + 4u, le), load_f32(p, byte_off + 8u, le), ); }} fn raw_fetch_f32_4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ return vec4f( load_f32(p, byte_off + 0u, le), load_f32(p, byte_off + 4u, le), load_f32(p, byte_off + 8u, le), load_f32(p, byte_off + 12u, le), ); }} fn fetch_u8_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ let v = raw_fetch_u8_1(p, byte_off); return f32(v) / f32(1u << frac); }} fn fetch_s8_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ let v = (bitcast(raw_fetch_u8_1(p, byte_off)) << 24) >> 24; return f32(v) / f32(1u << frac); }} fn fetch_u8_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ let v = raw_fetch_u8_2(p, byte_off); return vec2f(v) / f32(1u << frac); }} fn fetch_s8_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ let v = (bitcast(raw_fetch_u8_2(p, byte_off)) << vec2u(24u)) >> vec2u(24u); return vec2f(v) / f32(1u << frac); }} fn fetch_u8_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ let v = raw_fetch_u8_3(p, byte_off); return vec3f(v) / f32(1u << frac); }} fn fetch_s8_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ let v = (bitcast(raw_fetch_u8_3(p, byte_off)) << vec3u(24u)) >> vec3u(24u); return vec3f(v) / f32(1u << frac); }} fn fetch_u8_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ let v = raw_fetch_u8_4(p, byte_off); return vec4f(v) / f32(1u << frac); }} fn fetch_s8_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ let v = (bitcast(raw_fetch_u8_4(p, byte_off)) << vec4u(24u)) >> vec4u(24u); return vec4f(v) / f32(1u << frac); }} fn fetch_u16_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ let v = raw_fetch_u16_1(p, byte_off, le); return f32(v) / f32(1u << frac); }} fn fetch_s16_1(p: ptr>, byte_off: u32, frac: u32, le: bool) -> f32 {{ let v = bitcast(raw_fetch_u16_1(p, byte_off, le) << 16u) >> 16; return f32(v) / f32(1u << frac); }} fn fetch_u16_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ let v = raw_fetch_u16_2(p, byte_off, le); return vec2f(v) / f32(1u << frac); }} fn fetch_s16_2(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec2f {{ let v = (bitcast(raw_fetch_u16_2(p, byte_off, le)) << vec2u(16u)) >> vec2u(16u); return vec2f(v) / f32(1u << frac); }} fn fetch_u16_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ let v = raw_fetch_u16_3(p, byte_off, le); return vec3f(v) / f32(1u << frac); }} fn fetch_s16_3(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec3f {{ let v = (bitcast(raw_fetch_u16_3(p, byte_off, le)) << vec3u(16u)) >> vec3u(16u); return vec3f(v) / f32(1u << frac); }} fn fetch_u16_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ let v = raw_fetch_u16_4(p, byte_off, le); return vec4f(v) / f32(1u << frac); }} fn fetch_s16_4(p: ptr>, byte_off: u32, frac: u32, le: bool) -> vec4f {{ let v = (bitcast(raw_fetch_u16_4(p, byte_off, le)) << vec4u(16u)) >> vec4u(16u); return vec4f(v) / f32(1u << frac); }} fn fetch_f32_1(p: ptr>, byte_off: u32, le: bool) -> f32 {{ return raw_fetch_f32_1(p, byte_off, le); }} fn fetch_f32_2(p: ptr>, byte_off: u32, le: bool) -> vec2f {{ return raw_fetch_f32_2(p, byte_off, le); }} fn fetch_f32_3(p: ptr>, byte_off: u32, le: bool) -> vec3f {{ return raw_fetch_f32_3(p, byte_off, le); }} fn fetch_f32_4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ return raw_fetch_f32_4(p, byte_off, le); }} fn fetch_rgb565(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = load_u16(p, byte_off, le); return vec4f( f32((v >> 11u) & 0x1Fu) / f32(0x1Fu), f32((v >> 5u) & 0x3Fu) / f32(0x3Fu), f32((v >> 0u) & 0x1Fu) / f32(0x1Fu), 1.0, ); }} fn fetch_rgb8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = raw_fetch_u8_3(p, byte_off); return vec4f(f32(v.x), f32(v.y), f32(v.z), 255.0) / 255.0; }} fn fetch_rgbx8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = raw_fetch_u8_4(p, byte_off); return vec4f(f32(v.x), f32(v.y), f32(v.z), 255.0) / 255.0; }} fn fetch_rgba4(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = load_u16(p, byte_off, le); return vec4f( f32((v >> 12u) & 0x0Fu) / f32(0x0Fu), f32((v >> 8u) & 0x0Fu) / f32(0x0Fu), f32((v >> 4u) & 0x0Fu) / f32(0x0Fu), f32((v >> 0u) & 0x0Fu) / f32(0x0Fu), ); }} fn fetch_rgba6(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = load_u24(p, byte_off, le); return vec4f( f32((v >> 18u) & 0x3Fu) / f32(0x3Fu), f32((v >> 12u) & 0x3Fu) / f32(0x3Fu), f32((v >> 6u) & 0x3Fu) / f32(0x3Fu), f32((v >> 0u) & 0x3Fu) / f32(0x3Fu), ); }} fn fetch_rgba8(p: ptr>, byte_off: u32, le: bool) -> vec4f {{ let v = raw_fetch_u8_4(p, byte_off); return vec4f(v) / 255.0; }} fn tev_overflow_f32(in: f32) -> f32 {{ let byte_space = in * 255.0; return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; }} fn tev_overflow_vec3f(in: vec3f) -> vec3f {{ let byte_space = in * 255.0; return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; }} fn tev_overflow_vec4f(in: vec4f) -> vec4f {{ let byte_space = in * 255.0; return (byte_space - floor(byte_space / 256.0) * 256.0) / 255.0; }} {8} struct Uniform {{ vtx_start: u32, current_pnmtx: u32, render_viewport_size: vec2f, logical_viewport_size: vec2f, pad: vec2u, array_start: array,{0} }}; @group(0) @binding(0) var vbuf: array; @group(0) @binding(1) var abuf: array; @group(1) @binding(0) var ubuf: Uniform;{1} struct VertexOutput {{ @builtin(position) pos: vec4f,{2} }}; @vertex fn vs_main( @builtin(vertex_index) vidx: u32{3} ) -> VertexOutput {{ var out: VertexOutput;{7}{4} return out; }} @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f {{{6}{5} return prev; }} )""", uniBufAttrs, texBindings, vtxOutAttrs, vtxInAttrs, vtxXfrAttrs, fragmentFn, fragmentFnPre, vtxXfrAttrsPre, uniformPre); if (EnableDebugPrints) { Log.info("Generated shader (hash {:x}): {}", hash, shaderSource); } return shaderSource; } wgpu::ShaderModule build_shader(const ShaderConfig& config) noexcept { ZoneScoped; const auto shaderSource = build_shader_source(config); const auto hash = xxh3_hash(config); wgpu::ShaderSourceWGSL wgslDescriptor{}; wgslDescriptor.code = shaderSource.c_str(); const auto label = fmt::format("GX Shader {:x}", hash); const auto shaderDescriptor = wgpu::ShaderModuleDescriptor{ .nextInChain = &wgslDescriptor, .label = label.c_str(), }; return webgpu::g_device.CreateShaderModule(&shaderDescriptor); } } // namespace aurora::gx