#include "common.hpp" #include "../webgpu/gpu.hpp" #include "gx.hpp" #include constexpr bool EnableNormalVisualization = false; constexpr bool EnableDebugPrints = true; constexpr bool UsePerPixelLighting = true; namespace aurora::gfx::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_map> g_gxCachedShaders; #ifndef NDEBUG static absl::flat_hash_map g_gxCachedShaderConfigs; #endif 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 void color_arg_reg_info(GXTevColorArg arg, const TevStage& stage, ShaderInfo& info) { switch (arg) { case GX_CC_CPREV: case GX_CC_APREV: if (!info.writesTevReg.test(GX_TEVPREV)) { info.loadsTevReg.set(GX_TEVPREV); } break; case GX_CC_C0: case GX_CC_A0: if (!info.writesTevReg.test(GX_TEVREG0)) { info.loadsTevReg.set(GX_TEVREG0); } break; case GX_CC_C1: case GX_CC_A1: if (!info.writesTevReg.test(GX_TEVREG1)) { info.loadsTevReg.set(GX_TEVREG1); } break; case GX_CC_C2: case GX_CC_A2: if (!info.writesTevReg.test(GX_TEVREG2)) { info.loadsTevReg.set(GX_TEVREG2); } break; case GX_CC_TEXC: case GX_CC_TEXA: if (stage.texCoordId == GX_TEXCOORD_NULL) { Log.report(LOG_FATAL, FMT_STRING("texCoord not bound")); } if (stage.texMapId == GX_TEXMAP_NULL) { Log.report(LOG_FATAL, FMT_STRING("texMap not bound")); } info.sampledTexCoords.set(stage.texCoordId); info.sampledTextures.set(stage.texMapId); break; case GX_CC_RASC: case GX_CC_RASA: if (stage.channelId >= GX_COLOR0A0 && stage.channelId <= GX_COLOR1A1) { info.sampledColorChannels.set(stage.channelId - GX_COLOR0A0); } break; case GX_CC_KONST: switch (stage.kcSel) { case GX_TEV_KCSEL_K0: case GX_TEV_KCSEL_K0_R: case GX_TEV_KCSEL_K0_G: case GX_TEV_KCSEL_K0_B: case GX_TEV_KCSEL_K0_A: info.sampledKColors.set(0); break; case GX_TEV_KCSEL_K1: case GX_TEV_KCSEL_K1_R: case GX_TEV_KCSEL_K1_G: case GX_TEV_KCSEL_K1_B: case GX_TEV_KCSEL_K1_A: info.sampledKColors.set(1); break; case GX_TEV_KCSEL_K2: case GX_TEV_KCSEL_K2_R: case GX_TEV_KCSEL_K2_G: case GX_TEV_KCSEL_K2_B: case GX_TEV_KCSEL_K2_A: info.sampledKColors.set(2); break; case GX_TEV_KCSEL_K3: case GX_TEV_KCSEL_K3_R: case GX_TEV_KCSEL_K3_G: case GX_TEV_KCSEL_K3_B: case GX_TEV_KCSEL_K3_A: info.sampledKColors.set(3); break; default: break; } break; default: break; } } static bool formatHasAlpha(u32 format) { switch (format) { case GX_TF_IA4: case GX_TF_IA8: case GX_TF_RGB5A3: case GX_TF_RGBA8: case GX_TF_CMPR: case GX_CTF_RA4: case GX_CTF_RA8: case GX_CTF_YUVA8: case GX_CTF_A8: case GX_TF_RGBA8_PC: return true; default: return false; } } static std::string color_arg_reg(GXTevColorArg arg, size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { switch (arg) { case GX_CC_CPREV: return "prev.rgb"; case GX_CC_APREV: return "vec3(prev.a)"; case GX_CC_C0: return "tevreg0.rgb"; case GX_CC_A0: return "vec3(tevreg0.a)"; case GX_CC_C1: return "tevreg1.rgb"; case GX_CC_A1: return "vec3(tevreg1.a)"; case GX_CC_C2: return "tevreg2.rgb"; case GX_CC_A2: return "vec3(tevreg2.a)"; case GX_CC_TEXC: { if (stage.texMapId == GX_TEXMAP_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped texture for stage {}"), stageIdx); unreachable(); } else if (stage.texMapId < GX_TEXMAP0 || stage.texMapId > GX_TEXMAP7) { Log.report(LOG_FATAL, FMT_STRING("invalid texture {} for stage {}"), stage.texMapId, stageIdx); unreachable(); } const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format(FMT_STRING("sampled{}.{}{}{}"), stageIdx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); } case GX_CC_TEXA: { if (stage.texMapId == GX_TEXMAP_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped texture for stage {}"), stageIdx); unreachable(); } else if (stage.texMapId < GX_TEXMAP0 || stage.texMapId > GX_TEXMAP7) { Log.report(LOG_FATAL, FMT_STRING("invalid texture {} for stage {}"), stage.texMapId, stageIdx); unreachable(); } const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format(FMT_STRING("vec3(sampled{}.{})"), stageIdx, chan_comp(swap.alpha)); } case GX_CC_RASC: { if (stage.channelId == GX_COLOR_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped color channel for stage {}"), stageIdx); unreachable(); } else if (stage.channelId == GX_COLOR_ZERO) { return "vec3(0.0)"; } else if (stage.channelId < GX_COLOR0A0 || stage.channelId > GX_COLOR1A1) { Log.report(LOG_FATAL, FMT_STRING("invalid color channel {} for stage {}"), stage.channelId, stageIdx); unreachable(); } u32 idx = stage.channelId - GX_COLOR0A0; const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format(FMT_STRING("rast{}.{}{}{}"), idx, chan_comp(swap.red), chan_comp(swap.green), chan_comp(swap.blue)); } case GX_CC_RASA: { if (stage.channelId == GX_COLOR_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped color channel for stage {}"), stageIdx); unreachable(); } else if (stage.channelId == GX_COLOR_ZERO) { return "vec3(0.0)"; } else if (stage.channelId < GX_COLOR0A0 || stage.channelId > GX_COLOR1A1) { Log.report(LOG_FATAL, FMT_STRING("invalid color channel {} for stage {}"), stage.channelId, stageIdx); unreachable(); } u32 idx = stage.channelId - GX_COLOR0A0; const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format(FMT_STRING("vec3(rast{}.{})"), idx, chan_comp(swap.alpha)); } case GX_CC_ONE: return "vec3(1.0)"; case GX_CC_HALF: return "vec3(0.5)"; case GX_CC_KONST: { switch (stage.kcSel) { case GX_TEV_KCSEL_8_8: return "vec3(1.0)"; case GX_TEV_KCSEL_7_8: return "vec3(7.0/8.0)"; case GX_TEV_KCSEL_6_8: return "vec3(6.0/8.0)"; case GX_TEV_KCSEL_5_8: return "vec3(5.0/8.0)"; case GX_TEV_KCSEL_4_8: return "vec3(4.0/8.0)"; case GX_TEV_KCSEL_3_8: return "vec3(3.0/8.0)"; case GX_TEV_KCSEL_2_8: return "vec3(2.0/8.0)"; case GX_TEV_KCSEL_1_8: return "vec3(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 "vec3(ubuf.kcolor0.r)"; case GX_TEV_KCSEL_K1_R: return "vec3(ubuf.kcolor1.r)"; case GX_TEV_KCSEL_K2_R: return "vec3(ubuf.kcolor2.r)"; case GX_TEV_KCSEL_K3_R: return "vec3(ubuf.kcolor3.r)"; case GX_TEV_KCSEL_K0_G: return "vec3(ubuf.kcolor0.g)"; case GX_TEV_KCSEL_K1_G: return "vec3(ubuf.kcolor1.g)"; case GX_TEV_KCSEL_K2_G: return "vec3(ubuf.kcolor2.g)"; case GX_TEV_KCSEL_K3_G: return "vec3(ubuf.kcolor3.g)"; case GX_TEV_KCSEL_K0_B: return "vec3(ubuf.kcolor0.b)"; case GX_TEV_KCSEL_K1_B: return "vec3(ubuf.kcolor1.b)"; case GX_TEV_KCSEL_K2_B: return "vec3(ubuf.kcolor2.b)"; case GX_TEV_KCSEL_K3_B: return "vec3(ubuf.kcolor3.b)"; case GX_TEV_KCSEL_K0_A: return "vec3(ubuf.kcolor0.a)"; case GX_TEV_KCSEL_K1_A: return "vec3(ubuf.kcolor1.a)"; case GX_TEV_KCSEL_K2_A: return "vec3(ubuf.kcolor2.a)"; case GX_TEV_KCSEL_K3_A: return "vec3(ubuf.kcolor3.a)"; default: Log.report(LOG_FATAL, FMT_STRING("invalid kcSel {}"), stage.kcSel); unreachable(); } } case GX_CC_ZERO: return "vec3(0.0)"; default: Log.report(LOG_FATAL, FMT_STRING("invalid color arg {}"), arg); unreachable(); } } static void alpha_arg_reg_info(GXTevAlphaArg arg, const TevStage& stage, ShaderInfo& info) { switch (arg) { case GX_CA_APREV: if (!info.writesTevReg.test(GX_TEVPREV)) { info.loadsTevReg.set(GX_TEVPREV); } break; case GX_CA_A0: if (!info.writesTevReg.test(GX_TEVREG0)) { info.loadsTevReg.set(GX_TEVREG0); } break; case GX_CA_A1: if (!info.writesTevReg.test(GX_TEVREG1)) { info.loadsTevReg.set(GX_TEVREG1); } break; case GX_CA_A2: if (!info.writesTevReg.test(GX_TEVREG2)) { info.loadsTevReg.set(GX_TEVREG2); } break; case GX_CA_TEXA: if (stage.texCoordId == GX_TEXCOORD_NULL) { Log.report(LOG_FATAL, FMT_STRING("texCoord not bound")); } if (stage.texMapId == GX_TEXMAP_NULL) { Log.report(LOG_FATAL, FMT_STRING("texMap not bound")); } info.sampledTexCoords.set(stage.texCoordId); info.sampledTextures.set(stage.texMapId); break; case GX_CA_RASA: if (stage.channelId >= GX_COLOR0A0 && stage.channelId <= GX_COLOR1A1) { info.sampledColorChannels.set(stage.channelId - GX_COLOR0A0); } break; case GX_CA_KONST: switch (stage.kaSel) { case GX_TEV_KASEL_K0_R: case GX_TEV_KASEL_K0_G: case GX_TEV_KASEL_K0_B: case GX_TEV_KASEL_K0_A: info.sampledKColors.set(0); break; case GX_TEV_KASEL_K1_R: case GX_TEV_KASEL_K1_G: case GX_TEV_KASEL_K1_B: case GX_TEV_KASEL_K1_A: info.sampledKColors.set(1); break; case GX_TEV_KASEL_K2_R: case GX_TEV_KASEL_K2_G: case GX_TEV_KASEL_K2_B: case GX_TEV_KASEL_K2_A: info.sampledKColors.set(2); break; case GX_TEV_KASEL_K3_R: case GX_TEV_KASEL_K3_G: case GX_TEV_KASEL_K3_B: case GX_TEV_KASEL_K3_A: info.sampledKColors.set(3); break; default: break; } break; default: break; } } static std::string alpha_arg_reg(GXTevAlphaArg arg, size_t stageIdx, const ShaderConfig& config, const TevStage& stage) { switch (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: { if (stage.texMapId == GX_TEXMAP_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped texture for stage {}"), stageIdx); unreachable(); } else if (stage.texMapId < GX_TEXMAP0 || stage.texMapId > GX_TEXMAP7) { Log.report(LOG_FATAL, FMT_STRING("invalid texture {} for stage {}"), stage.texMapId, stageIdx); unreachable(); } const auto& swap = config.tevSwapTable[stage.tevSwapTex]; return fmt::format(FMT_STRING("sampled{}.{}"), stageIdx, chan_comp(swap.alpha)); } case GX_CA_RASA: { if (stage.channelId == GX_COLOR_NULL) { Log.report(LOG_FATAL, FMT_STRING("unmapped color channel for stage {}"), stageIdx); unreachable(); } else if (stage.channelId == GX_COLOR_ZERO) { return "0.0"; } else if (stage.channelId < GX_COLOR0A0 || stage.channelId > GX_COLOR1A1) { Log.report(LOG_FATAL, FMT_STRING("invalid color channel {} for stage {}"), stage.channelId, stageIdx); unreachable(); } u32 idx = stage.channelId - GX_COLOR0A0; const auto& swap = config.tevSwapTable[stage.tevSwapRas]; return fmt::format(FMT_STRING("rast{}.{}"), idx, chan_comp(swap.alpha)); } case GX_CA_KONST: { switch (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"; default: Log.report(LOG_FATAL, FMT_STRING("invalid kaSel {}"), stage.kaSel); unreachable(); } } case GX_CA_ZERO: return "0.0"; default: Log.report(LOG_FATAL, FMT_STRING("invalid alpha arg {}"), arg); unreachable(); } } static std::string_view tev_op(GXTevOp op) { switch (op) { case GX_TEV_ADD: return ""sv; case GX_TEV_SUB: return "-"sv; default: Log.report(LOG_FATAL, FMT_STRING("TODO {}"), op); unreachable(); } } static std::string_view tev_bias(GXTevBias bias) { switch (bias) { case GX_TB_ZERO: return ""sv; case GX_TB_ADDHALF: return " + 0.5"sv; case GX_TB_SUBHALF: return " - 0.5"sv; default: Log.report(LOG_FATAL, FMT_STRING("invalid bias {}"), bias); unreachable(); } } static std::string alpha_compare(GXCompare comp, u8 ref, bool& valid) { const float fref = ref / 255.f; switch (comp) { case GX_NEVER: return "false"s; case GX_LESS: return fmt::format(FMT_STRING("(prev.a < {}f)"), fref); case GX_LEQUAL: return fmt::format(FMT_STRING("(prev.a <= {}f)"), fref); case GX_EQUAL: return fmt::format(FMT_STRING("(prev.a == {}f)"), fref); case GX_NEQUAL: return fmt::format(FMT_STRING("(prev.a != {}f)"), fref); case GX_GEQUAL: return fmt::format(FMT_STRING("(prev.a >= {}f)"), fref); case GX_GREATER: return fmt::format(FMT_STRING("(prev.a > {}f)"), fref); case GX_ALWAYS: valid = false; return "true"s; default: Log.report(LOG_FATAL, FMT_STRING("invalid compare {}"), comp); unreachable(); } } static std::string_view tev_scale(GXTevScale scale) { switch (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; default: Log.report(LOG_FATAL, FMT_STRING("invalid scale {}"), scale); unreachable(); } } static inline std::string vtx_attr(const ShaderConfig& config, GXAttr attr) { const auto type = config.vtxAttrs[attr]; if (type == GX_NONE) { if (attr == GX_VA_NRM) { // Default normal return "vec3(1.0, 0.0, 0.0)"s; } Log.report(LOG_FATAL, FMT_STRING("unmapped attr {}"), attr); unreachable(); } 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(FMT_STRING("in_clr{}"), idx); } if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { const auto idx = attr - GX_VA_TEX0; return fmt::format(FMT_STRING("in_tex{}_uv"), idx); } Log.report(LOG_FATAL, FMT_STRING("unhandled attr {}"), attr); unreachable(); } static inline std::string texture_conversion(const TextureConfig& tex, u32 stageIdx, u32 texMapId) { std::string out; if (tex.renderTex) switch (tex.copyFmt) { default: break; case GX_TF_RGB565: // Set alpha channel to 1.0 out += fmt::format(FMT_STRING("\n sampled{0}.a = 1.0;"), stageIdx); break; case GX_TF_I4: case GX_TF_I8: // FIXME HACK if (!is_palette_format(tex.loadFmt)) { // Perform intensity conversion out += fmt::format(FMT_STRING("\n sampled{0} = vec4(intensityF32(sampled{0}.rgb), 0.f, 0.f, 1.f);"), stageIdx); } break; } switch (tex.loadFmt) { default: break; case GX_TF_I4: case GX_TF_I8: // Splat R to RGBA out += fmt::format(FMT_STRING("\n sampled{0} = vec4(sampled{0}.r);"), stageIdx); break; } return out; } 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, }; constexpr std::array VtxAttributeNames{ "pn_mtx", "tex0_mtx", "tex1_mtx", "tex2_mtx", "tex3_mtx", "tex4_mtx", "tex5_mtx", "tex6_mtx", "tex7_mtx", "pos", "nrm", "clr0", "clr1", "tex0_uv", "tex1_uv", "tex2_uv", "tex3_uv", "tex4_uv", "tex5_uv", "tex6_uv", "tex7_uv", "pos_mtx_array", "nrm_mtx_array", "tex_mtx_array", "light_array", "nbt", }; ShaderInfo build_shader_info(const ShaderConfig& config) noexcept { // const auto hash = xxh3_hash(config); // const auto it = g_gxCachedShaders.find(hash); // if (it != g_gxCachedShaders.end()) { // return it->second.second; // } ShaderInfo info{ .uniformSize = 64 * 3, // mv, mvInv, proj }; for (int i = 0; i < config.tevStageCount; ++i) { const auto& stage = config.tevStages[i]; // Color pass color_arg_reg_info(stage.colorPass.a, stage, info); color_arg_reg_info(stage.colorPass.b, stage, info); color_arg_reg_info(stage.colorPass.c, stage, info); color_arg_reg_info(stage.colorPass.d, stage, info); info.writesTevReg.set(stage.colorOp.outReg); // Alpha pass alpha_arg_reg_info(stage.alphaPass.a, stage, info); alpha_arg_reg_info(stage.alphaPass.b, stage, info); alpha_arg_reg_info(stage.alphaPass.c, stage, info); alpha_arg_reg_info(stage.alphaPass.d, stage, info); if (!info.writesTevReg.test(stage.alphaOp.outReg)) { // If we're writing alpha to a register that's not been // written to in the shader, load from uniform buffer info.loadsTevReg.set(stage.alphaOp.outReg); info.writesTevReg.set(stage.alphaOp.outReg); } } info.uniformSize += info.loadsTevReg.count() * 16; bool lightingEnabled = false; for (int i = 0; i < info.sampledColorChannels.size(); ++i) { if (info.sampledColorChannels.test(i)) { const auto& cc = config.colorChannels[i * 2]; const auto& cca = config.colorChannels[i * 2 + 1]; if (cc.lightingEnabled || cca.lightingEnabled) { lightingEnabled = true; } } } if (lightingEnabled) { // Lights + light state for all channels info.uniformSize += 16 + (80 * GX::MaxLights); } for (int i = 0; i < info.sampledColorChannels.size(); ++i) { if (info.sampledColorChannels.test(i)) { const auto& cc = config.colorChannels[i * 2]; if (cc.lightingEnabled && cc.ambSrc == GX_SRC_REG) { info.uniformSize += 16; } if (cc.matSrc == GX_SRC_REG) { info.uniformSize += 16; } const auto& cca = config.colorChannels[i * 2 + 1]; if (cca.lightingEnabled && cca.ambSrc == GX_SRC_REG) { info.uniformSize += 16; } if (cca.matSrc == GX_SRC_REG) { info.uniformSize += 16; } } } info.uniformSize += info.sampledKColors.count() * 16; for (int i = 0; i < info.sampledTexCoords.size(); ++i) { if (!info.sampledTexCoords.test(i)) { continue; } const auto& tcg = config.tcgs[i]; if (tcg.mtx != GX_IDENTITY) { u32 texMtxIdx = (tcg.mtx - GX_TEXMTX0) / 3; info.usesTexMtx.set(texMtxIdx); info.texMtxTypes[texMtxIdx] = tcg.type; } if (tcg.postMtx != GX_PTIDENTITY) { u32 postMtxIdx = (tcg.postMtx - GX_PTTEXMTX0) / 3; info.usesPTTexMtx.set(postMtxIdx); } } for (int i = 0; i < info.usesTexMtx.size(); ++i) { if (info.usesTexMtx.test(i)) { switch (info.texMtxTypes[i]) { case GX_TG_MTX2x4: info.uniformSize += 32; break; case GX_TG_MTX3x4: info.uniformSize += 64; break; default: break; } } } info.uniformSize += info.usesPTTexMtx.count() * 64; if (config.fogType != GX_FOG_NONE) { info.usesFog = true; info.uniformSize += 32; } info.uniformSize += info.sampledTextures.count() * 4; info.uniformSize = align_uniform(info.uniformSize); return info; } WGPUShaderModule build_shader(const ShaderConfig& config, const ShaderInfo& info) noexcept { const auto hash = xxh3_hash(config); const auto it = g_gxCachedShaders.find(hash); if (it != g_gxCachedShaders.end()) { #ifndef NDEBUG if (g_gxCachedShaderConfigs[hash] != config) { Log.report(LOG_FATAL, FMT_STRING("Shader collision!")); unreachable(); } #endif return it->second.first; } if (EnableDebugPrints) { Log.report(LOG_INFO, FMT_STRING("Shader config (hash {:x}):"), hash); { for (int i = 0; i < config.tevStageCount; ++i) { const auto& stage = config.tevStages[i]; Log.report(LOG_INFO, FMT_STRING(" tevStages[{}]:"), i); Log.report(LOG_INFO, FMT_STRING(" color_a: {}"), TevColorArgNames[stage.colorPass.a]); Log.report(LOG_INFO, FMT_STRING(" color_b: {}"), TevColorArgNames[stage.colorPass.b]); Log.report(LOG_INFO, FMT_STRING(" color_c: {}"), TevColorArgNames[stage.colorPass.c]); Log.report(LOG_INFO, FMT_STRING(" color_d: {}"), TevColorArgNames[stage.colorPass.d]); Log.report(LOG_INFO, FMT_STRING(" alpha_a: {}"), TevAlphaArgNames[stage.alphaPass.a]); Log.report(LOG_INFO, FMT_STRING(" alpha_b: {}"), TevAlphaArgNames[stage.alphaPass.b]); Log.report(LOG_INFO, FMT_STRING(" alpha_c: {}"), TevAlphaArgNames[stage.alphaPass.c]); Log.report(LOG_INFO, FMT_STRING(" alpha_d: {}"), TevAlphaArgNames[stage.alphaPass.d]); Log.report(LOG_INFO, FMT_STRING(" color_op_clamp: {}"), stage.colorOp.clamp); Log.report(LOG_INFO, FMT_STRING(" color_op_op: {}"), stage.colorOp.op); Log.report(LOG_INFO, FMT_STRING(" color_op_bias: {}"), stage.colorOp.bias); Log.report(LOG_INFO, FMT_STRING(" color_op_scale: {}"), stage.colorOp.scale); Log.report(LOG_INFO, FMT_STRING(" color_op_reg_id: {}"), stage.colorOp.outReg); Log.report(LOG_INFO, FMT_STRING(" alpha_op_clamp: {}"), stage.alphaOp.clamp); Log.report(LOG_INFO, FMT_STRING(" alpha_op_op: {}"), stage.alphaOp.op); Log.report(LOG_INFO, FMT_STRING(" alpha_op_bias: {}"), stage.alphaOp.bias); Log.report(LOG_INFO, FMT_STRING(" alpha_op_scale: {}"), stage.alphaOp.scale); Log.report(LOG_INFO, FMT_STRING(" alpha_op_reg_id: {}"), stage.alphaOp.outReg); Log.report(LOG_INFO, FMT_STRING(" kc_sel: {}"), stage.kcSel); Log.report(LOG_INFO, FMT_STRING(" ka_sel: {}"), stage.kaSel); Log.report(LOG_INFO, FMT_STRING(" texCoordId: {}"), stage.texCoordId); Log.report(LOG_INFO, FMT_STRING(" texMapId: {}"), stage.texMapId); Log.report(LOG_INFO, FMT_STRING(" channelId: {}"), stage.channelId); } for (int i = 0; i < config.colorChannels.size(); ++i) { const auto& chan = config.colorChannels[i]; Log.report(LOG_INFO, FMT_STRING(" colorChannels[{}]: enabled {} mat {} amb {}"), 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.report(LOG_INFO, FMT_STRING(" tcg[{}]: src {} mtx {} post {} type {} norm {}"), i, tcg.src, tcg.mtx, tcg.postMtx, tcg.type, tcg.normalize); } } Log.report(LOG_INFO, FMT_STRING(" alphaCompare: comp0 {} ref0 {} op {} comp1 {} ref1 {}"), config.alphaCompare.comp0, config.alphaCompare.ref0, config.alphaCompare.op, config.alphaCompare.comp1, config.alphaCompare.ref1); Log.report(LOG_INFO, FMT_STRING(" indexedAttributeCount: {}"), config.indexedAttributeCount); Log.report(LOG_INFO, FMT_STRING(" fogType: {}"), config.fogType); } } std::string uniformPre; std::string uniBufAttrs; std::string uniformBindings; std::string sampBindings; std::string texBindings; std::string vtxOutAttrs; std::string vtxInAttrs; std::string vtxXfrAttrsPre; std::string vtxXfrAttrs; size_t locIdx = 0; size_t vtxOutIdx = 0; size_t uniBindingIdx = 1; if (config.indexedAttributeCount > 0) { // Display list attributes int currAttrIdx = 0; for (GXAttr attr{}; attr < MaxVtxAttr; attr = GXAttr(attr + 1)) { // Indexed attributes if (config.vtxAttrs[attr] != GX_INDEX8 && config.vtxAttrs[attr] != GX_INDEX16) { continue; } const auto [div, rem] = std::div(currAttrIdx, 4); std::string_view attrName; bool addUniformBinding = true; if (config.attrMapping[attr] != attr) { attrName = VtxAttributeNames[config.attrMapping[attr]]; addUniformBinding = false; } else { attrName = VtxAttributeNames[attr]; } vtxXfrAttrsPre += fmt::format(FMT_STRING("\n var {} = v_arr_{}[in_dl{}[{}]];"), vtx_attr(config, attr), attrName, div, rem); if (addUniformBinding) { std::string_view arrType; if (attr == GX_VA_POS || attr == GX_VA_NRM) { arrType = "vec3"; } else if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { arrType = "vec2"; } uniformBindings += fmt::format(FMT_STRING("\n@group(0) @binding({})" "\nvar v_arr_{}: array<{}>;"), uniBindingIdx++, attrName, arrType); } ++currAttrIdx; } auto [num4xAttrArrays, rem] = std::div(currAttrIdx, 4); u32 num2xAttrArrays = 0; if (rem > 2) { ++num4xAttrArrays; } else if (rem > 0) { num2xAttrArrays = 1; } for (u32 i = 0; i < num4xAttrArrays; ++i) { if (locIdx > 0) { vtxInAttrs += "\n , "; } else { vtxInAttrs += "\n "; } vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_dl{}: vec4"), locIdx++, i); } for (u32 i = 0; i < num2xAttrArrays; ++i) { if (locIdx > 0) { vtxInAttrs += "\n , "; } else { vtxInAttrs += "\n "; } vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_dl{}: vec2"), locIdx++, num4xAttrArrays + i); } } for (GXAttr attr{}; attr < MaxVtxAttr; attr = GXAttr(attr + 1)) { // Direct attributes if (config.vtxAttrs[attr] != GX_DIRECT) { continue; } if (locIdx > 0) { vtxInAttrs += "\n , "; } else { vtxInAttrs += "\n "; } if (attr == GX_VA_POS) { vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_pos: vec3"), locIdx++); } else if (attr == GX_VA_NRM) { vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_nrm: vec3"), locIdx++); } else if (attr == GX_VA_CLR0 || attr == GX_VA_CLR1) { vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_clr{}: vec4"), locIdx++, attr - GX_VA_CLR0); } else if (attr >= GX_VA_TEX0 && attr <= GX_VA_TEX7) { vtxInAttrs += fmt::format(FMT_STRING("@location({}) in_tex{}_uv: vec2"), locIdx++, attr - GX_VA_TEX0); } } vtxXfrAttrsPre += fmt::format(FMT_STRING("\n var mv_pos = ubuf.pos_mtx * vec4({}, 1.0);" "\n var mv_nrm = ubuf.nrm_mtx * vec4({}, 0.0);" "\n out.pos = ubuf.proj * vec4(mv_pos, 1.0);"), vtx_attr(config, GX_VA_POS), vtx_attr(config, GX_VA_NRM)); if constexpr (EnableNormalVisualization) { vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) nrm: vec3,"), vtxOutIdx++); vtxXfrAttrsPre += "\n out.nrm = mv_nrm;"; } std::string fragmentFnPre; std::string fragmentFn; for (u32 idx = 0; idx < config.tevStageCount; ++idx) { const auto& stage = config.tevStages[idx]; { std::string outReg; switch (stage.colorOp.outReg) { case GX_TEVPREV: outReg = "prev"; break; case GX_TEVREG0: outReg = "tevreg0"; break; case GX_TEVREG1: outReg = "tevreg1"; break; case GX_TEVREG2: outReg = "tevreg2"; break; default: Log.report(LOG_FATAL, FMT_STRING("invalid colorOp outReg {}"), stage.colorOp.outReg); } std::string op = fmt::format( FMT_STRING("(({4}mix({0}, {1}, {2}) + {3}){5}){6}"), 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), tev_op(stage.colorOp.op), tev_bias(stage.colorOp.bias), tev_scale(stage.colorOp.scale)); if (stage.colorOp.clamp) { op = fmt::format(FMT_STRING("clamp({}, vec3(0.0), vec3(1.0))"), op); } fragmentFn += fmt::format(FMT_STRING("\n // TEV stage {2}\n {0} = vec4({1}, {0}.a);"), outReg, op, idx); } { std::string outReg; switch (stage.alphaOp.outReg) { case GX_TEVPREV: outReg = "prev.a"; break; case GX_TEVREG0: outReg = "tevreg0.a"; break; case GX_TEVREG1: outReg = "tevreg1.a"; break; case GX_TEVREG2: outReg = "tevreg2.a"; break; default: Log.report(LOG_FATAL, FMT_STRING("invalid alphaOp outReg {}"), stage.alphaOp.outReg); } std::string op = fmt::format( FMT_STRING("(({4}mix({0}, {1}, {2}) + {3}){5}){6}"), 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), tev_op(stage.alphaOp.op), tev_bias(stage.alphaOp.bias), tev_scale(stage.alphaOp.scale)); if (stage.alphaOp.clamp) { op = fmt::format(FMT_STRING("clamp({}, 0.0, 1.0)"), op); } fragmentFn += fmt::format(FMT_STRING("\n {0} = {1};"), outReg, op); } } if (info.loadsTevReg.test(0)) { uniBufAttrs += "\n tevprev: vec4,"; fragmentFnPre += "\n var prev = ubuf.tevprev;"; } else { fragmentFnPre += "\n var prev: vec4;"; } for (int i = 1 /* Skip TEVPREV */; i < info.loadsTevReg.size(); ++i) { if (info.loadsTevReg.test(i)) { uniBufAttrs += fmt::format(FMT_STRING("\n tevreg{}: vec4,"), i - 1); fragmentFnPre += fmt::format(FMT_STRING("\n var tevreg{0} = ubuf.tevreg{0};"), i - 1); } else if (info.writesTevReg.test(i)) { fragmentFnPre += fmt::format(FMT_STRING("\n var tevreg{0}: vec4;"), i - 1); } } bool addedLightStruct = false; int vtxColorIdx = 0; for (int i = 0; i < info.sampledColorChannels.size(); ++i) { if (!info.sampledColorChannels.test(i)) { continue; } const auto& cc = config.colorChannels[i * 2]; const auto& cca = config.colorChannels[i * 2 + 1]; if (!addedLightStruct && (cc.lightingEnabled || cca.lightingEnabled)) { uniBufAttrs += fmt::format(FMT_STRING("\n lights: array," "\n lightState0: u32," "\n lightState0a: u32," "\n lightState1: u32," "\n lightState1a: u32,"), GX::MaxLights); uniformPre += "\n" "struct Light {\n" " pos: vec3,\n" " dir: vec3,\n" " color: vec4,\n" " cos_att: vec3,\n" " dist_att: vec3,\n" "};"; if (UsePerPixelLighting) { vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) mv_pos: vec3,"), vtxOutIdx++); vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) mv_nrm: vec3,"), vtxOutIdx++); vtxXfrAttrs += fmt::format(FMT_STRING(R"""( out.mv_pos = mv_pos; out.mv_nrm = mv_nrm;)""")); } addedLightStruct = true; } if (cc.lightingEnabled && cc.ambSrc == GX_SRC_REG) { uniBufAttrs += fmt::format(FMT_STRING("\n cc{0}_amb: vec4,"), i); } if (cc.matSrc == GX_SRC_REG) { uniBufAttrs += fmt::format(FMT_STRING("\n cc{0}_mat: vec4,"), i); } if (cca.lightingEnabled && cca.ambSrc == GX_SRC_REG) { uniBufAttrs += fmt::format(FMT_STRING("\n cc{0}a_amb: vec4,"), i); } if (cca.matSrc == GX_SRC_REG) { uniBufAttrs += fmt::format(FMT_STRING("\n cc{0}a_mat: vec4,"), i); } // Output vertex color if necessary bool usesVtxColor = false; if (((cc.lightingEnabled && cc.ambSrc == GX_SRC_VTX) || cc.matSrc == GX_SRC_VTX || (cca.lightingEnabled && cca.matSrc == GX_SRC_VTX) || cca.matSrc == GX_SRC_VTX)) { if (UsePerPixelLighting) { vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) clr{}: vec4,"), vtxOutIdx++, vtxColorIdx); vtxXfrAttrs += fmt::format(FMT_STRING("\n out.clr{} = {};"), vtxColorIdx, vtx_attr(config, static_cast(GX_VA_CLR0 + vtxColorIdx))); } usesVtxColor = true; } // TODO handle alpha lighting if (cc.lightingEnabled) { std::string ambSrc, matSrc, lightAttnFn, lightDiffFn; if (cc.ambSrc == GX_SRC_VTX) { if (UsePerPixelLighting) { ambSrc = fmt::format(FMT_STRING("in.clr{}"), vtxColorIdx); } else { ambSrc = vtx_attr(config, static_cast(GX_VA_CLR0 + vtxColorIdx)); } } else if (cc.ambSrc == GX_SRC_REG) { ambSrc = fmt::format(FMT_STRING("ubuf.cc{0}_amb"), i); } if (cc.matSrc == GX_SRC_VTX) { if (UsePerPixelLighting) { matSrc = fmt::format(FMT_STRING("in.clr{}"), vtxColorIdx); } else { matSrc = vtx_attr(config, static_cast(GX_VA_CLR0 + vtxColorIdx)); } } else if (cc.matSrc == GX_SRC_REG) { matSrc = fmt::format(FMT_STRING("ubuf.cc{0}_mat"), i); } GXDiffuseFn diffFn = cc.diffFn; if (cc.attnFn == GX_AF_NONE) { lightAttnFn = "attn = 1.0;"; } else if (cc.attnFn == GX_AF_SPOT) { lightAttnFn = fmt::format(FMT_STRING(R"""( var cosine = max(0.0, dot(ldir, light.dir)); var cos_attn = dot(light.cos_att, vec3(1.0, cosine, cosine * cosine)); var dist_attn = dot(light.dist_att, vec3(1.0, dist, dist2)); attn = max(0.0, cos_attn / dist_attn);)""")); } else if (cc.attnFn == GX_AF_SPEC) { diffFn = GX_DF_NONE; Log.report(LOG_FATAL, FMT_STRING("AF_SPEC unimplemented")); } if (diffFn == GX_DF_NONE) { lightDiffFn = "1.0"; } else if (diffFn == GX_DF_SIGN) { if (UsePerPixelLighting) { lightDiffFn = "dot(ldir, in.mv_nrm)"; } else { lightDiffFn = "dot(ldir, mv_nrm)"; } } else if (diffFn == GX_DF_CLAMP) { if (UsePerPixelLighting) { lightDiffFn = "max(0.0, dot(ldir, in.mv_nrm))"; } else { lightDiffFn = "max(0.0, dot(ldir, mv_nrm))"; } } std::string outVar, posVar; if (UsePerPixelLighting) { outVar = fmt::format(FMT_STRING("rast{}"), i); posVar = "in.mv_pos"; } else { outVar = fmt::format(FMT_STRING("out.cc{}"), i); posVar = "mv_pos"; } auto lightFunc = fmt::format(FMT_STRING(R"""( {{ var lighting = {5}; for (var i = 0u; i < {1}u; i++) {{ if ((ubuf.lightState{0} & (1u << i)) == 0u) {{ continue; }} var light = ubuf.lights[i]; var ldir = light.pos - {7}; 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); }} // TODO alpha lighting {6} = vec4(({4} * clamp(lighting, vec4(0.0), vec4(1.0))).xyz, {4}.a); }})"""), i, GX::MaxLights, lightAttnFn, lightDiffFn, matSrc, ambSrc, outVar, posVar); if (UsePerPixelLighting) { fragmentFnPre += fmt::format(FMT_STRING("\n var rast{}: vec4;"), i); fragmentFnPre += lightFunc; } else { vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) cc{}: vec4,"), vtxOutIdx++, i); vtxXfrAttrs += lightFunc; fragmentFnPre += fmt::format(FMT_STRING("\n var rast{0} = in.cc{0};"), i); } } else if (cc.matSrc == GX_SRC_VTX) { if (UsePerPixelLighting) { // Color will already be written to clr{} fragmentFnPre += fmt::format(FMT_STRING("\n var rast{0} = in.clr{0};"), vtxColorIdx); } else { vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) cc{}: vec4,"), vtxOutIdx++, i); vtxXfrAttrs += fmt::format(FMT_STRING("\n out.cc{} = {};"), i, vtx_attr(config, GXAttr(GX_VA_CLR0 + vtxColorIdx))); fragmentFnPre += fmt::format(FMT_STRING("\n var rast{0} = in.cc{0};"), i); } } else { fragmentFnPre += fmt::format(FMT_STRING("\n var rast{0} = ubuf.cc{0}_mat;"), i); } if (usesVtxColor) { ++vtxColorIdx; } } for (int i = 0; i < info.sampledKColors.size(); ++i) { if (info.sampledKColors.test(i)) { uniBufAttrs += fmt::format(FMT_STRING("\n kcolor{}: vec4,"), i); } } for (int i = 0; i < info.sampledTexCoords.size(); ++i) { if (!info.sampledTexCoords.test(i)) { continue; } const auto& tcg = config.tcgs[i]; vtxOutAttrs += fmt::format(FMT_STRING("\n @location({}) tex{}_uv: vec2,"), vtxOutIdx++, i); if (tcg.src >= GX_TG_TEX0 && tcg.src <= GX_TG_TEX7) { vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{} = vec4({}, 0.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(FMT_STRING("\n var tc{} = vec4(in_pos, 1.0);"), i); } else if (tcg.src == GX_TG_NRM) { vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{} = vec4(in_nrm, 1.0);"), i); } else { Log.report(LOG_FATAL, FMT_STRING("unhandled tcg src {} for "), tcg.src); unreachable(); } if (tcg.mtx == GX_IDENTITY) { vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{0}_tmp = tc{0}.xyz;"), i); } else { u32 texMtxIdx = (tcg.mtx - GX_TEXMTX0) / 3; vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{0}_tmp = ubuf.texmtx{1} * tc{0};"), i, texMtxIdx); } if (tcg.normalize) { vtxXfrAttrs += fmt::format(FMT_STRING("\n tc{0}_tmp = normalize(tc{0}_tmp);"), i); } if (tcg.postMtx == GX_PTIDENTITY) { vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{0}_proj = tc{0}_tmp;"), i); } else { u32 postMtxIdx = (tcg.postMtx - GX_PTTEXMTX0) / 3; vtxXfrAttrs += fmt::format(FMT_STRING("\n var tc{0}_proj = ubuf.postmtx{1} * vec4(tc{0}_tmp.xyz, 1.0);"), i, postMtxIdx); } vtxXfrAttrs += fmt::format(FMT_STRING("\n out.tex{0}_uv = tc{0}_proj.xy;"), i); } for (int i = 0; i < config.tevStages.size(); ++i) { const auto& stage = config.tevStages[i]; if (stage.texMapId == GX_TEXMAP_NULL || stage.texCoordId == GX_TEXCOORD_NULL // TODO should check this per-stage probably || !info.sampledTextures.test(stage.texMapId)) { continue; } std::string uvIn = fmt::format(FMT_STRING("in.tex{0}_uv"), stage.texCoordId); const auto& texConfig = config.textureConfig[stage.texMapId]; if (is_palette_format(texConfig.loadFmt)) { std::string_view suffix; if (!is_palette_format(texConfig.copyFmt)) { switch (texConfig.loadFmt) { case GX_TF_C4: suffix = "I4"sv; break; // case GX_TF_C8: // suffix = "I8"; // break; // case GX_TF_C14X2: // suffix = "I14X2"; // break; default: Log.report(LOG_FATAL, FMT_STRING("Unsupported palette format {}"), texConfig.loadFmt); unreachable(); } } fragmentFnPre += fmt::format(FMT_STRING("\n var sampled{0} = textureSamplePalette{3}(tex{1}, tex{1}_samp, {2}, tlut{1});"), i, stage.texMapId, uvIn, suffix); } else { fragmentFnPre += fmt::format( FMT_STRING("\n var sampled{0} = textureSampleBias(tex{1}, tex{1}_samp, {2}, ubuf.tex{1}_lod);"), i, stage.texMapId, uvIn); } fragmentFnPre += texture_conversion(texConfig, i, stage.texMapId); } for (int i = 0; i < info.usesTexMtx.size(); ++i) { if (info.usesTexMtx.test(i)) { switch (info.texMtxTypes[i]) { case GX_TG_MTX2x4: uniBufAttrs += fmt::format(FMT_STRING("\n texmtx{}: mat4x2,"), i); break; case GX_TG_MTX3x4: uniBufAttrs += fmt::format(FMT_STRING("\n texmtx{}: mat4x3,"), i); break; default: Log.report(LOG_FATAL, FMT_STRING("unhandled tex mtx type {}"), info.texMtxTypes[i]); unreachable(); } } } for (int i = 0; i < info.usesPTTexMtx.size(); ++i) { if (info.usesPTTexMtx.test(i)) { uniBufAttrs += fmt::format(FMT_STRING("\n postmtx{}: mat4x3,"), i); } } if (info.usesFog) { uniformPre += "\n" "struct Fog {\n" " color: vec4,\n" " a: f32,\n" " b: f32,\n" " c: f32,\n" " pad: f32,\n" "}"; uniBufAttrs += "\n fog: Fog,"; fragmentFn += "\n // Fog\n var fogF = clamp((ubuf.fog.a / (ubuf.fog.b - in.pos.z)) - ubuf.fog.c, 0.0, 1.0);"; switch (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; default: Log.report(LOG_FATAL, FMT_STRING("invalid fog type {}"), config.fogType); unreachable(); } fragmentFn += "\n prev = vec4(mix(prev.rgb, ubuf.fog.color.rgb, clamp(fogZ, 0.0, 1.0)), prev.a);"; } size_t texBindIdx = 0; for (int i = 0; i < info.sampledTextures.size(); ++i) { if (!info.sampledTextures.test(i)) { continue; } uniBufAttrs += fmt::format(FMT_STRING("\n tex{}_lod: f32,"), i); sampBindings += fmt::format(FMT_STRING("\n@group(1) @binding({})\n" "var tex{}_samp: sampler;"), texBindIdx, i); const auto& texConfig = config.textureConfig[i]; if (is_palette_format(texConfig.loadFmt)) { texBindings += fmt::format(FMT_STRING("\n@group(2) @binding({})\n" "var tex{}: texture_2d<{}>;"), texBindIdx, i, is_palette_format(texConfig.copyFmt) ? "i32"sv : "f32"sv); ++texBindIdx; texBindings += fmt::format(FMT_STRING("\n@group(2) @binding({})\n" "var tlut{}: texture_2d;"), texBindIdx, i); } else { texBindings += fmt::format(FMT_STRING("\n@group(2) @binding({})\n" "var tex{}: texture_2d;"), texBindIdx, i); } ++texBindIdx; } if (config.alphaCompare) { bool comp0Valid = true; bool comp1Valid = true; std::string comp0 = alpha_compare(config.alphaCompare.comp0, config.alphaCompare.ref0, comp0Valid); std::string comp1 = alpha_compare(config.alphaCompare.comp1, config.alphaCompare.ref1, comp1Valid); if (comp0Valid || comp1Valid) { fragmentFn += "\n // Alpha compare"; switch (config.alphaCompare.op) { case GX_AOP_AND: fragmentFn += fmt::format(FMT_STRING("\n if (!({} && {})) {{ discard; }}"), comp0, comp1); break; case GX_AOP_OR: fragmentFn += fmt::format(FMT_STRING("\n if (!({} || {})) {{ discard; }}"), comp0, comp1); break; case GX_AOP_XOR: fragmentFn += fmt::format(FMT_STRING("\n if (!({} ^^ {})) {{ discard; }}"), comp0, comp1); break; case GX_AOP_XNOR: fragmentFn += fmt::format(FMT_STRING("\n if (({} ^^ {})) {{ discard; }}"), comp0, comp1); break; default: Log.report(LOG_FATAL, FMT_STRING("invalid alpha compare op {}"), config.alphaCompare.op); unreachable(); } } } if constexpr (EnableNormalVisualization) { fragmentFn += "\n prev = vec4(in.nrm, prev.a);"; } const auto shaderSource = fmt::format(FMT_STRING(R"""({10} struct Uniform {{ pos_mtx: mat4x3, nrm_mtx: mat4x3, proj: mat4x4,{0} }}; @group(0) @binding(0) var ubuf: Uniform;{3}{1}{2} struct VertexOutput {{ @builtin(position) pos: vec4,{4} }}; fn intensityF32(rgb: vec3) -> f32 {{ // RGB to intensity conversion // https://github.com/dolphin-emu/dolphin/blob/4cd48e609c507e65b95bca5afb416b59eaf7f683/Source/Core/VideoCommon/TextureConverterShaderGen.cpp#L237-L241 return dot(rgb, vec3(0.257, 0.504, 0.098)) + 16.0 / 255.0; }} fn intensityI4(rgb: vec3) -> i32 {{ return i32(intensityF32(rgb) * 16.f); }} fn textureSamplePalette(tex: texture_2d, samp: sampler, uv: vec2, tlut: texture_2d) -> vec4 {{ // Gather index values var i = textureGather(0, tex, samp, uv); // Load palette colors var c0 = textureLoad(tlut, vec2(i[0], 0), 0); var c1 = textureLoad(tlut, vec2(i[1], 0), 0); var c2 = textureLoad(tlut, vec2(i[2], 0), 0); var c3 = textureLoad(tlut, vec2(i[3], 0), 0); // Perform bilinear filtering var f = fract(uv * vec2(textureDimensions(tex)) + 0.5); var t0 = mix(c3, c2, f.x); var t1 = mix(c0, c1, f.x); return mix(t0, t1, f.y); }} fn textureSamplePaletteI4(tex: texture_2d, samp: sampler, uv: vec2, tlut: texture_2d) -> vec4 {{ // Gather RGB channels var iR = textureGather(0, tex, samp, uv); var iG = textureGather(1, tex, samp, uv); var iB = textureGather(2, tex, samp, uv); // Perform intensity conversion var i0 = intensityI4(vec3(iR[0], iG[0], iB[0])); var i1 = intensityI4(vec3(iR[1], iG[1], iB[1])); var i2 = intensityI4(vec3(iR[2], iG[2], iB[2])); var i3 = intensityI4(vec3(iR[3], iG[3], iB[3])); // Load palette colors var c0 = textureLoad(tlut, vec2(i0, 0), 0); var c1 = textureLoad(tlut, vec2(i1, 0), 0); var c2 = textureLoad(tlut, vec2(i2, 0), 0); var c3 = textureLoad(tlut, vec2(i3, 0), 0); // Perform bilinear filtering var f = fract(uv * vec2(textureDimensions(tex)) + 0.5); var t0 = mix(c3, c2, f.x); var t1 = mix(c0, c1, f.x); return mix(t0, t1, f.y); }} @stage(vertex) fn vs_main({5} ) -> VertexOutput {{ var out: VertexOutput;{9}{6} return out; }} @stage(fragment) fn fs_main(in: VertexOutput) -> @location(0) vec4 {{{8}{7} return prev; }} )"""), uniBufAttrs, sampBindings, texBindings, uniformBindings, vtxOutAttrs, vtxInAttrs, vtxXfrAttrs, fragmentFn, fragmentFnPre, vtxXfrAttrsPre, uniformPre); if (EnableDebugPrints) { Log.report(LOG_INFO, FMT_STRING("Generated shader: {}"), shaderSource); } const WGPUShaderModuleWGSLDescriptor wgslDescriptor{ .chain = {.sType = WGPUSType_ShaderModuleWGSLDescriptor}, .source = shaderSource.c_str(), }; const auto label = fmt::format(FMT_STRING("GX Shader {:x}"), hash); const auto shaderDescriptor = WGPUShaderModuleDescriptor{ .nextInChain = &wgslDescriptor.chain, .label = label.c_str(), }; auto shader = wgpuDeviceCreateShaderModule(webgpu::g_device, &shaderDescriptor); auto pair = std::make_pair(shader, info); g_gxCachedShaders.emplace(hash, pair); #ifndef NDEBUG g_gxCachedShaderConfigs.emplace(hash, config); #endif return pair.first; } } // namespace aurora::gfx::gx