#include "gx/mtl/CGxDeviceMTL.hpp" #include "app/mac/View.h" #include "gx/Buffer.hpp" #include "gx/CGxBatch.hpp" #include "gx/Window.hpp" #include "gx/texture/CGxTex.hpp" #include "math/Utils.hpp" #include "util/Autorelease.hpp" #include #include #include #include #include #include #include #import #import #import namespace { MTLPrimitiveType MtlPrimitiveType(EGxPrim prim) { switch (prim) { case GxPrim_Points: return MTLPrimitiveTypePoint; case GxPrim_Lines: return MTLPrimitiveTypeLine; case GxPrim_LineStrip: return MTLPrimitiveTypeLineStrip; case GxPrim_Triangles: return MTLPrimitiveTypeTriangle; case GxPrim_TriangleStrip: return MTLPrimitiveTypeTriangleStrip; case GxPrim_TriangleFan: return MTLPrimitiveTypeTriangle; default: return MTLPrimitiveTypeTriangle; } } bool GxTexIsCompressed(EGxTexFormat format) { return format == GxTex_Dxt1 || format == GxTex_Dxt3 || format == GxTex_Dxt5; } MTLPixelFormat MtlPixelFormatForGx(EGxTexFormat format) { // Note: GxTex format names describe the packed 32-bit value layout (MSB to LSB). // On little-endian, this means the byte order in memory is reversed. // Metal pixel formats describe byte order in memory. // GxTex_Argb8888: packed as 0xAARRGGBB, bytes in memory: BB GG RR AA -> use BGRA // GxTex_Abgr8888: packed as 0xAABBGGRR, bytes in memory: RR GG BB AA -> use RGBA switch (format) { case GxTex_Abgr8888: return MTLPixelFormatRGBA8Unorm; case GxTex_Argb8888: return MTLPixelFormatBGRA8Unorm; case GxTex_Argb4444: // Convert ARGB4444 to RGBA8 during upload for simpler handling return MTLPixelFormatRGBA8Unorm; case GxTex_Argb1555: return MTLPixelFormatBGR5A1Unorm; case GxTex_Rgb565: return MTLPixelFormatB5G6R5Unorm; case GxTex_Dxt1: return MTLPixelFormatBC1_RGBA; case GxTex_Dxt3: return MTLPixelFormatBC2_RGBA; case GxTex_Dxt5: return MTLPixelFormatBC3_RGBA; case GxTex_Uv88: return MTLPixelFormatRG8Unorm; case GxTex_Gr1616F: return MTLPixelFormatRG16Float; case GxTex_R32F: return MTLPixelFormatR32Float; default: return MTLPixelFormatInvalid; } } MTLBlendFactor MtlBlendFactorForSrc(EGxBlend blend) { switch (blend) { case GxBlend_Alpha: case GxBlend_Add: case GxBlend_SrcAlphaOpaque: return MTLBlendFactorSourceAlpha; case GxBlend_Mod: case GxBlend_Mod2x: case GxBlend_ModAdd: return MTLBlendFactorDestinationColor; case GxBlend_InvSrcAlphaAdd: case GxBlend_InvSrcAlphaOpaque: return MTLBlendFactorOneMinusSourceAlpha; case GxBlend_NoAlphaAdd: case GxBlend_Opaque: case GxBlend_AlphaKey: return MTLBlendFactorOne; case GxBlend_ConstantAlpha: return MTLBlendFactorBlendAlpha; default: return MTLBlendFactorOne; } } MTLBlendFactor MtlBlendFactorForDst(EGxBlend blend) { switch (blend) { case GxBlend_Alpha: return MTLBlendFactorOneMinusSourceAlpha; case GxBlend_Add: case GxBlend_ModAdd: case GxBlend_InvSrcAlphaAdd: case GxBlend_NoAlphaAdd: case GxBlend_ConstantAlpha: return MTLBlendFactorOne; case GxBlend_Mod: return MTLBlendFactorZero; case GxBlend_Mod2x: return MTLBlendFactorSourceColor; case GxBlend_InvSrcAlphaOpaque: case GxBlend_SrcAlphaOpaque: case GxBlend_Opaque: case GxBlend_AlphaKey: return MTLBlendFactorZero; default: return MTLBlendFactorZero; } } void ApplyViewport(CGxDeviceMTL* device, id encoder, id texture) { const double width = texture.width; const double height = texture.height; double x = (device->m_viewport.x.l * width) + 0.5; double y = ((1.0 - device->m_viewport.y.h) * height) + 0.5; double w = (device->m_viewport.x.h * width) - x + 0.5; double h = ((1.0 - device->m_viewport.y.l) * height) - y + 0.5; if (w < 0.0) { w = 0.0; } if (h < 0.0) { h = 0.0; } MTLViewport viewport = { x, y, w, h, device->m_viewport.z.l, device->m_viewport.z.h }; [encoder setViewport:viewport]; NSUInteger sx = x < 0.0 ? 0 : static_cast(x); NSUInteger sy = y < 0.0 ? 0 : static_cast(y); NSUInteger sw = static_cast(w); NSUInteger sh = static_cast(h); if (sx > texture.width) { sx = texture.width; } if (sy > texture.height) { sy = texture.height; } if (sx + sw > texture.width) { sw = texture.width - sx; } if (sy + sh > texture.height) { sh = texture.height - sy; } MTLScissorRect scissor = { sx, sy, sw, sh }; [encoder setScissorRect:scissor]; device->intF6C = 0; } const char kMetalShaderSource[] = "#include \n" "using namespace metal;\n" "struct VertexColorIn {\n" " float3 position [[attribute(0)]];\n" " float4 color [[attribute(1)]];\n" "};\n" "struct VertexTexIn {\n" " float3 position [[attribute(0)]];\n" " float2 texcoord [[attribute(1)]];\n" "};\n" "struct VertexTex2In {\n" " float3 position [[attribute(0)]];\n" " float2 texcoord [[attribute(1)]];\n" " float2 texcoord1 [[attribute(2)]];\n" "};\n" "struct VertexColorTexIn {\n" " float3 position [[attribute(0)]];\n" " float4 color [[attribute(1)]];\n" " float2 texcoord [[attribute(2)]];\n" "};\n" "struct VertexColorTex2In {\n" " float3 position [[attribute(0)]];\n" " float4 color [[attribute(1)]];\n" " float2 texcoord [[attribute(2)]];\n" " float2 texcoord1 [[attribute(3)]];\n" "};\n" "struct VertexSolidIn {\n" " float3 position [[attribute(0)]];\n" "};\n" "struct VertexSkinIn {\n" " float3 position [[attribute(0)]];\n" " float4 weights [[attribute(1)]];\n" " uchar4 indices [[attribute(2)]];\n" "};\n" "struct VertexSkinTexIn {\n" " float3 position [[attribute(0)]];\n" " float4 weights [[attribute(1)]];\n" " uchar4 indices [[attribute(2)]];\n" " float2 texcoord [[attribute(3)]];\n" "};\n" "struct VertexSkinTex2In {\n" " float3 position [[attribute(0)]];\n" " float4 weights [[attribute(1)]];\n" " uchar4 indices [[attribute(2)]];\n" " float2 texcoord [[attribute(3)]];\n" " float2 texcoord1 [[attribute(4)]];\n" "};\n" "struct VertexOut {\n" " float4 position [[position]];\n" " float pointSize [[point_size]];\n" " float4 color;\n" " float2 texcoord;\n" " float2 texcoord1;\n" " float viewZ;\n" "};\n" "struct PSConstants {\n" " float alphaRef;\n" " float fogStart;\n" " float fogEnd;\n" " float fogEnabled;\n" " float4 color;\n" " float4 fogColor;\n" "};\n" "struct VSConstants {\n" " float4x4 mvp;\n" " float pointSize;\n" " float pad[3];\n" "};\n" "// Transform UV using texture matrix from vertex constants\n" "// Texture matrix 0: vc[6] and vc[7] (2 rows for tex unit 0)\n" "// Texture matrix 1: vc[8] and vc[9] (2 rows for tex unit 1)\n" "float2 transformTexCoord(float2 uv, constant float4* vc, uint unit) {\n" " uint base = 6 + unit * 2;\n" " float4 row0 = vc[base];\n" " float4 row1 = vc[base + 1];\n" " // Check if matrix is uninitialized (identity placeholder)\n" " if (row0.x > 1e30) return uv;\n" " float4 uvw = float4(uv, 0.0, 1.0);\n" " return float2(dot(uvw, row0), dot(uvw, row1));\n" "}\n" "vertex VertexOut vs_color(VertexColorIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = in.color;\n" " out.texcoord = float2(0.0, 0.0);\n" " out.texcoord1 = float2(0.0, 0.0);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_tex(VertexTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_tex2(VertexTex2In in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord1, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_color_tex(VertexColorTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = in.color;\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_color_tex2(VertexColorTex2In in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = in.color;\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord1, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_solid(VertexSolidIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" " VertexOut out;\n" " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" " out.position = pos4;\n" " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" " out.texcoord = float2(0.0, 0.0);\n" " out.texcoord1 = float2(0.0, 0.0);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_skin(VertexSkinIn in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos = float4(in.position, 1.0);\n" " float4 w = in.weights;\n" " uint4 idx = uint4(in.indices);\n" " float3 skinned = float3(0.0);\n" " for (uint i = 0; i < 4; ++i) {\n" " uint base = 31 + idx[i] * 3;\n" " float4 c0 = vc[base + 0];\n" " float4 c1 = vc[base + 1];\n" " float4 c2 = vc[base + 2];\n" " float3 p;\n" " p.x = dot(pos, c0);\n" " p.y = dot(pos, c1);\n" " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" " // Read diffuse (vc[28]) and emissive (vc[29]) for color animation\n" " float4 diffuse = vc[28];\n" " float4 emissive = vc[29];\n" " float4 color = diffuse + emissive;\n" " out.color = (diffuse.x > 1e30) ? float4(1.0, 1.0, 1.0, 1.0) : color;\n" " out.texcoord = float2(0.0, 0.0);\n" " out.texcoord1 = float2(0.0, 0.0);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_skin_tex(VertexSkinTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos = float4(in.position, 1.0);\n" " float4 w = in.weights;\n" " uint4 idx = uint4(in.indices);\n" " float3 skinned = float3(0.0);\n" " for (uint i = 0; i < 4; ++i) {\n" " uint base = 31 + idx[i] * 3;\n" " float4 c0 = vc[base + 0];\n" " float4 c1 = vc[base + 1];\n" " float4 c2 = vc[base + 2];\n" " float3 p;\n" " p.x = dot(pos, c0);\n" " p.y = dot(pos, c1);\n" " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" " // Read diffuse (vc[28]) and emissive (vc[29]) for color animation\n" " float4 diffuse = vc[28];\n" " float4 emissive = vc[29];\n" " float4 color = diffuse + emissive;\n" " out.color = (diffuse.x > 1e30) ? float4(1.0, 1.0, 1.0, 1.0) : color;\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "vertex VertexOut vs_skin_tex2(VertexSkinTex2In in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" " VertexOut out;\n" " float4 pos = float4(in.position, 1.0);\n" " float4 w = in.weights;\n" " uint4 idx = uint4(in.indices);\n" " float3 skinned = float3(0.0);\n" " for (uint i = 0; i < 4; ++i) {\n" " uint base = 31 + idx[i] * 3;\n" " float4 c0 = vc[base + 0];\n" " float4 c1 = vc[base + 1];\n" " float4 c2 = vc[base + 2];\n" " float3 p;\n" " p.x = dot(pos, c0);\n" " p.y = dot(pos, c1);\n" " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" " // Read diffuse (vc[28]) and emissive (vc[29]) for color animation\n" " float4 diffuse = vc[28];\n" " float4 emissive = vc[29];\n" " float4 color = diffuse + emissive;\n" " out.color = (diffuse.x > 1e30) ? float4(1.0, 1.0, 1.0, 1.0) : color;\n" " out.texcoord = transformTexCoord(in.texcoord, vc, 0);\n" " out.texcoord1 = transformTexCoord(in.texcoord1, vc, 1);\n" " out.viewZ = pos4.w;\n" " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" "float4 applyFog(float4 color, float viewZ, constant PSConstants& ps) {\n" " if (ps.fogEnabled > 0.0 && ps.fogEnd > ps.fogStart) {\n" " float fogFactor = saturate((ps.fogEnd - viewZ) / (ps.fogEnd - ps.fogStart));\n" " color.rgb = mix(ps.fogColor.rgb, color.rgb, fogFactor);\n" " }\n" " return color;\n" "}\n" "fragment float4 ps_main(VertexOut in [[stage_in]], constant PSConstants& ps [[buffer(0)]]) {\n" " float4 color = in.color * ps.color;\n" " if (ps.alphaRef > 0.0 && color.a < ps.alphaRef) {\n" " discard_fragment();\n" " }\n" " return applyFog(color, in.viewZ, ps);\n" "}\n" "fragment float4 ps_tex1(VertexOut in [[stage_in]], texture2d tex0 [[texture(0)]], sampler samp0 [[sampler(0)]], constant PSConstants& ps [[buffer(0)]]) {\n" " float4 color = tex0.sample(samp0, in.texcoord) * in.color * ps.color;\n" " if (ps.alphaRef > 0.0 && color.a < ps.alphaRef) {\n" " discard_fragment();\n" " }\n" " return applyFog(color, in.viewZ, ps);\n" "}\n" "fragment float4 ps_tex(VertexOut in [[stage_in]], texture2d tex0 [[texture(0)]], sampler samp0 [[sampler(0)]], texture2d tex1 [[texture(1)]], sampler samp1 [[sampler(1)]], constant PSConstants& ps [[buffer(0)]]) {\n" " float4 color = tex0.sample(samp0, in.texcoord) * tex1.sample(samp1, in.texcoord1) * in.color * ps.color;\n" " if (ps.alphaRef > 0.0 && color.a < ps.alphaRef) {\n" " discard_fragment();\n" " }\n" " return applyFog(color, in.viewZ, ps);\n" "}\n"; } CGxDeviceMTL::CGxDeviceMTL() : CGxDevice() { this->m_api = GxApi_Metal; this->m_caps.m_colorFormat = GxCF_rgba; this->DeviceCreatePools(); this->DeviceCreateStreamBufs(); } void CGxDeviceMTL::ITexMarkAsUpdated(CGxTex* texId) { if (texId->m_needsFlagUpdate && texId->m_apiSpecificData2) { auto sampler = (id)texId->m_apiSpecificData2; [sampler release]; texId->m_apiSpecificData2 = nullptr; } if (texId->m_needsUpdate) { if (texId->m_needsCreation || !texId->m_apiSpecificData) { this->ITexCreate(texId); } if (!texId->m_needsCreation && texId->m_apiSpecificData && texId->m_userFunc) { this->ITexUpload(texId); } CGxDevice::ITexMarkAsUpdated(texId); } } void CGxDeviceMTL::IRsSendToHw(EGxRenderState which) { auto* state = &this->m_appRenderStates[which]; switch (which) { // Texture states - mark textures as needing update case GxRs_Texture0: case GxRs_Texture1: case GxRs_Texture2: case GxRs_Texture3: case GxRs_Texture4: case GxRs_Texture5: case GxRs_Texture6: case GxRs_Texture7: case GxRs_Texture8: case GxRs_Texture9: case GxRs_Texture10: case GxRs_Texture11: case GxRs_Texture12: case GxRs_Texture13: case GxRs_Texture14: case GxRs_Texture15: { CGxTex* texture = static_cast(static_cast(state->m_value)); if (texture) { this->ITexMarkAsUpdated(texture); } break; } // States handled at draw time - no immediate GPU action needed case GxRs_BlendingMode: case GxRs_AlphaRef: case GxRs_DepthTest: case GxRs_DepthFunc: case GxRs_DepthWrite: case GxRs_Culling: case GxRs_Fog: case GxRs_FogStart: case GxRs_FogEnd: case GxRs_FogColor: case GxRs_Lighting: case GxRs_VertexShader: case GxRs_PixelShader: case GxRs_ColorWrite: case GxRs_ScissorTest: // Metal applies these during Draw() call break; // States not yet implemented case GxRs_PolygonOffset: case GxRs_MatDiffuse: case GxRs_MatEmissive: case GxRs_MatSpecular: case GxRs_MatSpecularExp: case GxRs_NormalizeNormals: case GxRs_ClipPlaneMask: case GxRs_Multisample: case GxRs_ColorOp0: case GxRs_ColorOp1: case GxRs_ColorOp2: case GxRs_ColorOp3: case GxRs_ColorOp4: case GxRs_ColorOp5: case GxRs_ColorOp6: case GxRs_ColorOp7: case GxRs_AlphaOp0: case GxRs_AlphaOp1: case GxRs_AlphaOp2: case GxRs_AlphaOp3: case GxRs_AlphaOp4: case GxRs_AlphaOp5: case GxRs_AlphaOp6: case GxRs_AlphaOp7: case GxRs_TexGen0: case GxRs_TexGen1: case GxRs_TexGen2: case GxRs_TexGen3: case GxRs_TexGen4: case GxRs_TexGen5: case GxRs_TexGen6: case GxRs_TexGen7: case GxRs_PointScale: case GxRs_PointScaleAttenuation: case GxRs_PointScaleMin: case GxRs_PointScaleMax: case GxRs_PointSprite: case GxRs_ColorMaterial: // Not implemented in Metal backend break; default: // Unknown state break; } } int32_t CGxDeviceMTL::DeviceCreate(int32_t (*windowProc)(void* window, uint32_t message, uintptr_t wparam, intptr_t lparam), const CGxFormat& format) { System_Autorelease::ScopedPool autorelease; CGRect rect; Rect* bounds; Rect* zoomedBounds = GetSavedZoomedWindowBounds(); if ( zoomedBounds && zoomedBounds->bottom - zoomedBounds->top > 599 && zoomedBounds->right - zoomedBounds->left > 799 ) { bounds = GetSavedZoomedWindowBounds(); } else { bounds = GetSavedWindowBounds(); } if ( bounds->bottom - bounds->top > 599 && bounds->right - bounds->left > 799 ) { rect.origin.x = bounds->left; rect.origin.y = bounds->top; rect.size.width = bounds->right - bounds->left; rect.size.height = bounds->bottom - bounds->top; } else { Rect newBounds = { 0, 0, static_cast(std::floor((static_cast(format.size.y) / static_cast(format.size.x)) * 1024.0f)), 1024, }; SetSavedWindowBounds(newBounds); rect.origin.x = newBounds.left; rect.origin.y = newBounds.top; rect.size.width = newBounds.right; rect.size.height = newBounds.bottom; } this->m_window.SetViewClass(GetEngineViewClass()); this->m_window.Init(rect, nullptr); this->m_window.SetTitle("World of Warcraft"); this->m_window.CreateView(); id device = MTLCreateSystemDefaultDevice(); if (!device) { return 0; } id commandQueue = [device newCommandQueue]; if (!commandQueue) { return 0; } NSView* view = this->m_window.GetNSView(); CAMetalLayer* layer = view ? (CAMetalLayer*)[view layer] : nil; if (!layer) { return 0; } layer.device = device; layer.pixelFormat = MTLPixelFormatBGRA8Unorm; layer.framebufferOnly = YES; CGSize drawableSize = [view convertSizeToBacking:view.bounds.size]; layer.drawableSize = drawableSize; this->m_device = device; this->m_commandQueue = commandQueue; this->m_layer = layer; if (CGxDevice::DeviceCreate(windowProc, format)) { auto callbacks = new GLWindowCallbacks(); AssignEngineViewCallbacks(callbacks); this->m_window.SetCallbacks(callbacks); this->m_window.Show(); this->ISetCaps(format); this->m_context = 1; this->ICursorCreate(format); return 1; } return 0; } void CGxDeviceMTL::ISetCaps(const CGxFormat& format) { (void)format; this->m_caps.m_pixelCenterOnEdge = 1; this->m_caps.m_texelCenterOnEdge = 1; this->m_caps.m_colorFormat = GxCF_rgba; this->m_caps.m_generateMipMaps = 1; this->m_caps.int10 = 1; this->m_caps.m_texFmt[GxTex_Dxt1] = 1; this->m_caps.m_texFmt[GxTex_Dxt3] = 1; this->m_caps.m_texFmt[GxTex_Dxt5] = 1; this->m_caps.m_shaderTargets[GxSh_Vertex] = GxShVS_arbvp1; this->m_caps.m_shaderTargets[GxSh_Pixel] = GxShPS_arbfp1; this->m_caps.m_texFilterAnisotropic = 1; this->m_caps.m_maxTexAnisotropy = 16; for (int32_t i = 0; i < GxTexTargets_Last; ++i) { this->m_caps.m_texTarget[i] = 1; this->m_caps.m_texMaxSize[i] = 4096; } } int32_t CGxDeviceMTL::DeviceSetFormat(const CGxFormat& format) { CGxDevice::DeviceSetFormat(format); CRect rect = { 0.0f, 0.0f, static_cast(format.size.y), static_cast(format.size.x) }; this->DeviceSetDefWindow(rect); if (this->m_window.m_Window) { this->m_window.Resize(format.size.x, format.size.y); } return 1; } void CGxDeviceMTL::EnsureLibrary() { if (this->m_shaderLibrary || !this->m_device) { return; } auto device = (id)this->m_device; NSString* source = [[NSString alloc] initWithUTF8String:kMetalShaderSource]; NSError* error = nil; id library = [device newLibraryWithSource:source options:nil error:&error]; [source release]; if (!library) { return; } this->m_shaderLibrary = library; } void* CGxDeviceMTL::GetPipeline(EGxVertexBufferFormat format, bool useColor, bool useSkin, bool useTex, int32_t blendMode) { if (format >= GxVertexBufferFormats_Last || !this->m_device || !this->m_layer) { return nullptr; } if (blendMode < 0 || blendMode >= GxBlends_Last) { blendMode = GxBlend_Opaque; } this->EnsureLibrary(); if (!this->m_shaderLibrary) { return nullptr; } auto device = (id)this->m_device; auto layer = (CAMetalLayer*)this->m_layer; auto library = (id)this->m_shaderLibrary; NSString* vsName = nil; NSString* psName = nil; bool hasPosition = false; bool hasColor = false; bool hasBlendWeight = false; bool hasBlendIndices = false; bool hasTex0 = false; bool hasTex1 = false; uint32_t posOffset = 0; uint32_t colorOffset = 0; uint32_t blendWeightOffset = 0; uint32_t blendIndexOffset = 0; uint32_t tex0Offset = 0; uint32_t tex1Offset = 0; auto& bufDesc = Buffer::s_vertexBufDesc[format]; for (uint32_t i = 0; i < bufDesc.attribCount; ++i) { const auto& attrib = bufDesc.attribs[i]; if (attrib.attrib == GxVA_Position) { posOffset = attrib.offset; hasPosition = true; } else if (attrib.attrib == GxVA_BlendWeight) { blendWeightOffset = attrib.offset; hasBlendWeight = true; } else if (attrib.attrib == GxVA_BlendIndices) { blendIndexOffset = attrib.offset; hasBlendIndices = true; } else if (attrib.attrib == GxVA_Color0) { colorOffset = attrib.offset; hasColor = true; } else if (attrib.attrib == GxVA_TexCoord0) { tex0Offset = attrib.offset; hasTex0 = true; } else if (attrib.attrib == GxVA_TexCoord1) { tex1Offset = attrib.offset; hasTex1 = true; } } bool useTexPipeline = useTex && hasTex0; bool useTex2Pipeline = useTexPipeline && hasTex1; void* (*pipelineTable)[GxBlends_Last] = nullptr; if (useSkin) { if (useTex2Pipeline) { pipelineTable = this->m_pipelineSkinTex2; } else if (useTexPipeline) { pipelineTable = this->m_pipelineSkinTex; } else { pipelineTable = this->m_pipelineSkin; } } else if (useColor) { if (useTex2Pipeline) { pipelineTable = this->m_pipelineColorTex2; } else if (useTexPipeline) { pipelineTable = this->m_pipelineColorTex; } else { pipelineTable = this->m_pipelineColor; } } else { if (useTex2Pipeline) { pipelineTable = this->m_pipelineSolidTex2; } else if (useTexPipeline) { pipelineTable = this->m_pipelineSolidTex; } else { pipelineTable = this->m_pipelineSolid; } } if (pipelineTable[format][blendMode]) { return pipelineTable[format][blendMode]; } if (useSkin) { if (useTex2Pipeline) { vsName = @"vs_skin_tex2"; } else { vsName = useTexPipeline ? @"vs_skin_tex" : @"vs_skin"; } } else if (useColor) { if (useTex2Pipeline) { vsName = @"vs_color_tex2"; } else { vsName = useTexPipeline ? @"vs_color_tex" : @"vs_color"; } } else { if (useTex2Pipeline) { vsName = @"vs_tex2"; } else { vsName = useTexPipeline ? @"vs_tex" : @"vs_solid"; } } // Select pixel shader based on texture usage: // - ps_main: no textures // - ps_tex1: single texture (tex0 only) // - ps_tex: two textures (tex0 * tex1) if (useTex2Pipeline) { psName = @"ps_tex"; } else if (useTexPipeline) { psName = @"ps_tex1"; } else { psName = @"ps_main"; } id vs = [library newFunctionWithName:vsName]; id ps = [library newFunctionWithName:psName]; if (!vs || !ps) { if (vs) { [vs release]; } if (ps) { [ps release]; } return nullptr; } auto desc = [MTLRenderPipelineDescriptor new]; desc.vertexFunction = vs; desc.fragmentFunction = ps; desc.colorAttachments[0].pixelFormat = layer.pixelFormat; desc.depthAttachmentPixelFormat = MTLPixelFormatDepth32Float; auto vdesc = [MTLVertexDescriptor vertexDescriptor]; if (!hasPosition || (useColor && !hasColor) || (useSkin && (!hasBlendWeight || !hasBlendIndices)) || (useTexPipeline && !hasTex0) || (useTex2Pipeline && !hasTex1)) { [desc release]; [vs release]; [ps release]; return nullptr; } vdesc.attributes[0].format = MTLVertexFormatFloat3; vdesc.attributes[0].offset = posOffset; vdesc.attributes[0].bufferIndex = 0; if (useSkin) { vdesc.attributes[1].format = MTLVertexFormatUChar4Normalized; vdesc.attributes[1].offset = blendWeightOffset; vdesc.attributes[1].bufferIndex = 0; vdesc.attributes[2].format = MTLVertexFormatUChar4; vdesc.attributes[2].offset = blendIndexOffset; vdesc.attributes[2].bufferIndex = 0; if (useTexPipeline) { vdesc.attributes[3].format = MTLVertexFormatFloat2; vdesc.attributes[3].offset = tex0Offset; vdesc.attributes[3].bufferIndex = 0; if (useTex2Pipeline) { vdesc.attributes[4].format = MTLVertexFormatFloat2; vdesc.attributes[4].offset = tex1Offset; vdesc.attributes[4].bufferIndex = 0; } } } else if (useColor) { vdesc.attributes[1].format = MTLVertexFormatUChar4Normalized; vdesc.attributes[1].offset = colorOffset; vdesc.attributes[1].bufferIndex = 0; if (useTexPipeline) { vdesc.attributes[2].format = MTLVertexFormatFloat2; vdesc.attributes[2].offset = tex0Offset; vdesc.attributes[2].bufferIndex = 0; if (useTex2Pipeline) { vdesc.attributes[3].format = MTLVertexFormatFloat2; vdesc.attributes[3].offset = tex1Offset; vdesc.attributes[3].bufferIndex = 0; } } } else if (useTexPipeline) { vdesc.attributes[1].format = MTLVertexFormatFloat2; vdesc.attributes[1].offset = tex0Offset; vdesc.attributes[1].bufferIndex = 0; if (useTex2Pipeline) { vdesc.attributes[2].format = MTLVertexFormatFloat2; vdesc.attributes[2].offset = tex1Offset; vdesc.attributes[2].bufferIndex = 0; } } vdesc.layouts[0].stride = bufDesc.size; vdesc.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex; vdesc.layouts[0].stepRate = 1; desc.vertexDescriptor = vdesc; bool enableBlend = blendMode > GxBlend_AlphaKey; desc.colorAttachments[0].blendingEnabled = enableBlend ? YES : NO; if (enableBlend) { desc.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd; desc.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd; desc.colorAttachments[0].sourceRGBBlendFactor = MtlBlendFactorForSrc(static_cast(blendMode)); desc.colorAttachments[0].destinationRGBBlendFactor = MtlBlendFactorForDst(static_cast(blendMode)); desc.colorAttachments[0].sourceAlphaBlendFactor = desc.colorAttachments[0].sourceRGBBlendFactor; desc.colorAttachments[0].destinationAlphaBlendFactor = desc.colorAttachments[0].destinationRGBBlendFactor; } // ColorWrite is applied via writeMask; for now set all channels enabled. // A full implementation would need separate pipelines per write mask value. desc.colorAttachments[0].writeMask = MTLColorWriteMaskAll; NSError* error = nil; id pipeline = [device newRenderPipelineStateWithDescriptor:desc error:&error]; [desc release]; [vs release]; [ps release]; if (!pipeline) { return nullptr; } pipelineTable[format][blendMode] = pipeline; return pipeline; } void* CGxDeviceMTL::GetPoolBuffer(CGxPool* pool) { if (!pool || !this->m_device) { return nullptr; } auto device = (id)this->m_device; auto buffer = (id)pool->m_apiSpecific; if (!pool->m_mem) { pool->m_mem = SMemAlloc(pool->m_size, __FILE__, __LINE__, 0x0); } if (!buffer || buffer.length < static_cast(pool->m_size)) { if (buffer) { [buffer release]; } buffer = [device newBufferWithBytesNoCopy:pool->m_mem length:pool->m_size options:MTLResourceStorageModeShared deallocator:nil]; pool->m_apiSpecific = buffer; } return buffer; } void CGxDeviceMTL::BeginFrame() { if (this->m_frameEncoder || !this->m_device || !this->m_commandQueue || !this->m_layer) { return; } auto commandQueue = (id)this->m_commandQueue; auto layer = (CAMetalLayer*)this->m_layer; id drawable = [layer nextDrawable]; if (!drawable) { return; } const bool clearRequested = (this->m_clearMask & 0x1) != 0; const uint8_t r = (this->m_clearColor >> 16) & 0xFF; const uint8_t g = (this->m_clearColor >> 8) & 0xFF; const uint8_t b = this->m_clearColor & 0xFF; const uint8_t a = (this->m_clearColor >> 24) & 0xFF; auto pass = [MTLRenderPassDescriptor renderPassDescriptor]; pass.colorAttachments[0].texture = drawable.texture; pass.colorAttachments[0].loadAction = MTLLoadActionClear; pass.colorAttachments[0].storeAction = MTLStoreActionStore; pass.colorAttachments[0].clearColor = MTLClearColorMake( clearRequested ? r / 255.0f : 0.0f, clearRequested ? g / 255.0f : 0.0f, clearRequested ? b / 255.0f : 0.0f, clearRequested ? a / 255.0f : 1.0f ); this->EnsureDepthTexture(drawable.texture.width, drawable.texture.height); if (this->m_depthTexture) { auto depthTex = (id)this->m_depthTexture; pass.depthAttachment.texture = depthTex; pass.depthAttachment.loadAction = (this->m_clearMask & 0x2) ? MTLLoadActionClear : MTLLoadActionLoad; pass.depthAttachment.storeAction = MTLStoreActionStore; pass.depthAttachment.clearDepth = 1.0; } id commandBuffer = [commandQueue commandBuffer]; id encoder = [commandBuffer renderCommandEncoderWithDescriptor:pass]; ApplyViewport(this, encoder, drawable.texture); this->m_frameCommandBuffer = commandBuffer; this->m_frameEncoder = encoder; this->m_frameDrawable = drawable; this->m_frameHasDraw = 0; } void* CGxDeviceMTL::DeviceWindow() { return &this->m_window; } void CGxDeviceMTL::CapsWindowSize(CRect& rect) { CRect windowRect = this->DeviceCurWindow(); rect.minX = windowRect.minX; rect.minY = windowRect.minY; rect.maxX = windowRect.maxX; rect.maxY = windowRect.maxY; } void CGxDeviceMTL::CapsWindowSizeInScreenCoords(CRect& dst) { if (this->IDevIsWindowed()) { auto windowRect = this->DeviceCurWindow(); auto deviceRect = this->m_window.GetRect(); dst.minX = windowRect.minX + deviceRect.origin.x; dst.maxX = windowRect.maxX + deviceRect.origin.x; dst.minY = windowRect.minY + deviceRect.origin.y; dst.maxY = windowRect.maxY + deviceRect.origin.y; } else { dst = this->DeviceCurWindow(); } } void CGxDeviceMTL::SceneClear(uint32_t mask, CImVector color) { CGxDevice::SceneClear(mask, color); if (!this->m_context) { return; } this->m_clearMask = mask; this->m_clearColor = color.value; } void CGxDeviceMTL::ScenePresent() { if (!this->m_context) { return; } System_Autorelease::ScopedPool autorelease; if (!this->m_frameEncoder) { this->BeginFrame(); } if (!this->m_frameEncoder || !this->m_frameCommandBuffer || !this->m_frameDrawable) { return; } auto encoder = (id)this->m_frameEncoder; if (this->intF6C) { auto drawable = (id)this->m_frameDrawable; ApplyViewport(this, encoder, drawable.texture); } auto commandBuffer = (id)this->m_frameCommandBuffer; auto drawable = (id)this->m_frameDrawable; [encoder endEncoding]; [commandBuffer presentDrawable:drawable]; [commandBuffer commit]; this->m_frameCommandBuffer = nullptr; this->m_frameEncoder = nullptr; this->m_frameDrawable = nullptr; } void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { if (!this->m_context || !batch) { return; } this->BeginFrame(); if (!this->m_frameEncoder) { return; } auto encoder = (id)this->m_frameEncoder; auto vertexBuf = this->m_primVertexFormatBuf[GxVA_Position]; if (!vertexBuf) { return; } // Check ColorWrite - skip rendering if completely disabled int32_t colorWrite = static_cast(this->m_appRenderStates[GxRs_ColorWrite].m_value); bool colorWriteEnabled = this->MasterEnable(GxMasterEnable_ColorWrite); if (!colorWriteEnabled || colorWrite == 0) { return; // No color channels will be written } bool useColor = (this->m_primVertexMask & GxPrim_Color0) != 0; bool useSkin = this->m_primVertexFormat == GxVBF_PBNT2; bool useTex = false; // Check for texture coordinates in two ways: // 1. Standard vertex buffer format - check buffer descriptor // 2. Custom vertex format (m_primVertexFormat == Last) - check m_primVertexMask if (this->m_primVertexFormat < GxVertexBufferFormats_Last) { const auto& bufDesc = Buffer::s_vertexBufDesc[this->m_primVertexFormat]; for (uint32_t i = 0; i < bufDesc.attribCount; ++i) { if (bufDesc.attribs[i].attrib == GxVA_TexCoord0) { useTex = true; break; } } } // Also check the primitive vertex mask for TexCoord0 bit (covers custom formats) if (!useTex && (this->m_primVertexMask & (1 << GxVA_TexCoord0))) { useTex = true; } // Debug logging - enable with WHOA_GX_MTL_LOG_DRAW=1 static bool s_logDraw = std::getenv("WHOA_GX_MTL_LOG_DRAW") != nullptr; static int s_drawCount = 0; if (s_logDraw && s_drawCount < 500) { auto texState = static_cast(static_cast(this->m_appRenderStates[GxRs_Texture0].m_value)); int32_t blendModeDbg = static_cast(this->m_appRenderStates[GxRs_BlendingMode].m_value); fprintf(stderr, "[MTL Draw #%d] fmt=%d mask=0x%x useTex=%d useColor=%d blend=%d tex0=%p count=%d\n", s_drawCount++, this->m_primVertexFormat, this->m_primVertexMask, useTex, useColor, blendModeDbg, texState, batch->m_count); } int32_t blendMode = static_cast(this->m_appRenderStates[GxRs_BlendingMode].m_value); auto pipeline = (id)this->GetPipeline(this->m_primVertexFormat, useColor, useSkin, useTex, blendMode); if (!pipeline && useColor) { useColor = false; pipeline = (id)this->GetPipeline(this->m_primVertexFormat, false, useSkin, useTex, blendMode); } if (!pipeline) { return; } auto mtlVertexBuf = (id)this->GetPoolBuffer(vertexBuf->m_pool); if (!mtlVertexBuf) { return; } // VSConstants struct matches the Metal shader's VSConstants struct MtlVSConstants { C44Matrix mvp; float pointSize; float pad[3]; } vsConsts; vsConsts.pointSize = 1.0f; // Default point size bool useShaderMvp = false; // Restore shader constant usage for standard UI formats. if (!useSkin) { auto vsState = static_cast(static_cast(this->m_appRenderStates[GxRs_VertexShader].m_value)); if (vsState) { static bool s_logConstants = std::getenv("WHOA_GX_MTL_LOG_CONSTANTS") != nullptr; if (s_logConstants) { const auto& c = CGxDevice::s_shadowConstants[1].constants; fprintf(stderr, "VS Consts: [0] %f %f %f %f\n", c[0].x, c[0].y, c[0].z, c[0].w); fprintf(stderr, " [1] %f %f %f %f\n", c[1].x, c[1].y, c[1].z, c[1].w); fprintf(stderr, " [2] %f %f %f %f\n", c[2].x, c[2].y, c[2].z, c[2].w); fprintf(stderr, " [3] %f %f %f %f\n", c[3].x, c[3].y, c[3].z, c[3].w); } // NOTE: VS constant MVP from c[0-3] is disabled - it causes rendering issues. // The game may store MVP in constants for some shaders, but the format varies. // For now, we rely on the transform stack (world * view * proj). } } if (!useShaderMvp) { if (useSkin) { vsConsts.mvp = this->m_projNative; } else { const auto& world = this->m_xforms[GxXform_World].TopConst(); const auto& view = this->m_xforms[GxXform_View].TopConst(); vsConsts.mvp = (world * view) * this->m_projNative; } } // Get point size from render state if point sprites are enabled int32_t pointSprite = static_cast(this->m_appRenderStates[GxRs_PointSprite].m_value); if (pointSprite) { float pointScale = static_cast(static_cast(this->m_appRenderStates[GxRs_PointScale].m_value)); if (pointScale > 0.0f) { vsConsts.pointSize = pointScale; } } [encoder setRenderPipelineState:pipeline]; if (blendMode == GxBlend_ConstantAlpha) { float alphaRef = static_cast(static_cast(this->m_appRenderStates[GxRs_AlphaRef].m_value)) / 255.0f; [encoder setBlendColorRed:1.0 green:1.0 blue:1.0 alpha:alphaRef]; } int32_t depthTest = static_cast(this->m_appRenderStates[GxRs_DepthTest].m_value); uint32_t depthFunc = static_cast(this->m_appRenderStates[GxRs_DepthFunc].m_value); int32_t depthWrite = static_cast(this->m_appRenderStates[GxRs_DepthWrite].m_value); bool depthTestEnabled = this->MasterEnable(GxMasterEnable_DepthTest) && depthTest; bool depthWriteEnabled = this->MasterEnable(GxMasterEnable_DepthWrite) && depthWrite; auto depthState = (id)this->GetDepthState(depthTestEnabled, depthWriteEnabled, depthFunc); if (depthState) { [encoder setDepthStencilState:depthState]; } // Polygon offset (depth bias) for decals and overlapping geometry int32_t polygonOffset = static_cast(this->m_appRenderStates[GxRs_PolygonOffset].m_value); if (polygonOffset != 0) { // Apply depth bias to push geometry slightly back, avoiding z-fighting float slopeScale = static_cast(polygonOffset) * 2.0f; float units = static_cast(polygonOffset); [encoder setDepthBias:units slopeScale:slopeScale clamp:0.0f]; } else { [encoder setDepthBias:0.0f slopeScale:0.0f clamp:0.0f]; } int32_t cullMode = static_cast(this->m_appRenderStates[GxRs_Culling].m_value); if (cullMode == 0) { [encoder setCullMode:MTLCullModeNone]; } else { [encoder setCullMode:MTLCullModeBack]; // Swap winding: OpenGL uses opposite convention from what we originally had [encoder setFrontFacingWinding:(cullMode == 1) ? MTLWindingCounterClockwise : MTLWindingClockwise]; } // Scissor test: when disabled, use full framebuffer; when enabled, use viewport scissor int32_t scissorTest = static_cast(this->m_appRenderStates[GxRs_ScissorTest].m_value); if (!scissorTest) { auto layer = (CAMetalLayer*)this->m_layer; CGSize size = layer.drawableSize; MTLScissorRect fullRect = { 0, 0, static_cast(size.width), static_cast(size.height) }; [encoder setScissorRect:fullRect]; } // When scissor test is enabled, the scissor rect is set by IStateSyncScissorRect via BeginFrame [encoder setVertexBuffer:mtlVertexBuf offset:vertexBuf->m_index atIndex:0]; [encoder setVertexBytes:&vsConsts length:sizeof(vsConsts) atIndex:1]; // Pass vertex constants for texture matrix transforms (needed for animated textures) // and skinning. All texture shaders now use buffer(2) for texture matrix lookup. if (useSkin || useTex) { [encoder setVertexBytes:CGxDevice::s_shadowConstants[1].constants length:sizeof(CGxDevice::s_shadowConstants[1].constants) atIndex:2]; } if (useTex) { auto texState = static_cast(static_cast(this->m_appRenderStates[GxRs_Texture0].m_value)); auto texture = (id)this->GetTexture(texState); if (!texture) { this->EnsureFallbackTexture(); texture = (id)this->m_fallbackTexture; } auto sampler = (id)this->GetSampler(texState); if (!sampler) { sampler = (id)this->m_fallbackSampler; } // Debug tex binding if (s_logDraw && s_drawCount <= 10) { EGxTexFormat gxfmt = texState ? texState->m_format : GxTex_Unknown; fprintf(stderr, " [Tex0] ptr=%p mtlTex=%p dim=%lux%lu gxfmt=%d pixelFmt=%lu\n", texState, texture, texture.width, texture.height, gxfmt, (unsigned long)texture.pixelFormat); } [encoder setFragmentTexture:texture atIndex:0]; [encoder setFragmentSamplerState:sampler atIndex:0]; auto texState1 = static_cast(static_cast(this->m_appRenderStates[GxRs_Texture1].m_value)); auto texture1 = (id)this->GetTexture(texState1); if (!texture1) { this->EnsureFallbackTexture(); texture1 = (id)this->m_fallbackTexture; } auto sampler1 = (id)this->GetSampler(texState1); if (!sampler1) { sampler1 = (id)this->m_fallbackSampler; } [encoder setFragmentTexture:texture1 atIndex:1]; [encoder setFragmentSamplerState:sampler1 atIndex:1]; } struct MtlPSConstants { float alphaRef; float fogStart; float fogEnd; float fogEnabled; float color[4]; float fogColor[4]; } psConsts; psConsts.alphaRef = static_cast(static_cast(this->m_appRenderStates[GxRs_AlphaRef].m_value)) / 255.0f; // Read fog render states int32_t fogEnabled = static_cast(this->m_appRenderStates[GxRs_Fog].m_value); psConsts.fogEnabled = fogEnabled ? 1.0f : 0.0f; psConsts.fogStart = static_cast(static_cast(this->m_appRenderStates[GxRs_FogStart].m_value)); psConsts.fogEnd = static_cast(static_cast(this->m_appRenderStates[GxRs_FogEnd].m_value)); // Fog color is stored as CImVector (packed ARGB) uint32_t fogColorPacked = static_cast(this->m_appRenderStates[GxRs_FogColor].m_value); psConsts.fogColor[0] = static_cast((fogColorPacked >> 16) & 0xFF) / 255.0f; // R psConsts.fogColor[1] = static_cast((fogColorPacked >> 8) & 0xFF) / 255.0f; // G psConsts.fogColor[2] = static_cast((fogColorPacked >> 0) & 0xFF) / 255.0f; // B psConsts.fogColor[3] = static_cast((fogColorPacked >> 24) & 0xFF) / 255.0f; // A // Default color to white psConsts.color[0] = 1.0f; psConsts.color[1] = 1.0f; psConsts.color[2] = 1.0f; psConsts.color[3] = 1.0f; // Apply pixel shader constants if a pixel shader is active AND Lighting is DISABLED. // UI rendering typically disables lighting and uses c[0] for color modulation. // World rendering enables lighting and uses c[0] for lighting parameters (which we don't support yet in this simple backend), // so we default to white for world objects to avoid blown-out colors. auto psState = static_cast(static_cast(this->m_appRenderStates[GxRs_PixelShader].m_value)); int32_t lighting = static_cast(this->m_appRenderStates[GxRs_Lighting].m_value); if (psState) { static bool s_logConstants = std::getenv("WHOA_GX_MTL_LOG_CONSTANTS") != nullptr; if (s_logConstants) { const auto& c = CGxDevice::s_shadowConstants[0].constants; fprintf(stderr, "PS Consts (Light=%d, Blend=%d): [0] %f %f %f %f\n", lighting, blendMode, c[0].x, c[0].y, c[0].z, c[0].w); } if (lighting == 0) { const auto& c = CGxDevice::s_shadowConstants[0].constants[0]; // Apply shader constants if they appear initialized (not FLT_MAX from ShaderConstantsClear). // Zero values are valid - they mean "don't modulate" or intentional transparency. // Color values are typically in 0-1 range, using 2.0f threshold for HDR margin. if (std::abs(c.x) <= 2.0f && std::abs(c.y) <= 2.0f && std::abs(c.z) <= 2.0f && std::abs(c.w) <= 2.0f) { psConsts.color[0] = c.x; psConsts.color[1] = c.y; psConsts.color[2] = c.z; psConsts.color[3] = c.w; } } } // Debug PS constants color if (s_logDraw && s_drawCount <= 10) { int32_t depthTest = static_cast(this->m_appRenderStates[GxRs_DepthTest].m_value); int32_t depthWrite = static_cast(this->m_appRenderStates[GxRs_DepthWrite].m_value); fprintf(stderr, " [PSConsts] color=(%.2f, %.2f, %.2f, %.2f) alphaRef=%.3f lighting=%d depthTest=%d depthWrite=%d\n", psConsts.color[0], psConsts.color[1], psConsts.color[2], psConsts.color[3], psConsts.alphaRef, lighting, depthTest, depthWrite); } [encoder setFragmentBytes:&psConsts length:sizeof(psConsts) atIndex:0]; auto primitive = MtlPrimitiveType(batch->m_primType); if (indexed) { auto indexBuf = this->m_primIndexBuf; if (!indexBuf || indexBuf->m_itemSize != 2) { return; } auto mtlIndexBuf = (id)this->GetPoolBuffer(indexBuf->m_pool); if (!mtlIndexBuf) { return; } const NSUInteger indexOffset = indexBuf->m_index + (batch->m_start * indexBuf->m_itemSize); [encoder drawIndexedPrimitives:primitive indexCount:batch->m_count indexType:MTLIndexTypeUInt16 indexBuffer:mtlIndexBuf indexBufferOffset:indexOffset]; } else { [encoder drawPrimitives:primitive vertexStart:batch->m_start vertexCount:batch->m_count]; } this->m_frameHasDraw = 1; } void CGxDeviceMTL::PoolSizeSet(CGxPool* pool, uint32_t size) { if (!pool || pool->m_size >= static_cast(size)) { return; } pool->m_size = static_cast(size); pool->unk1C = 0; if (pool->m_apiSpecific) { auto buffer = (id)pool->m_apiSpecific; [buffer release]; pool->m_apiSpecific = nullptr; } if (pool->m_mem) { SMemFree(pool->m_mem, __FILE__, __LINE__, 0x0); pool->m_mem = nullptr; } } char* CGxDeviceMTL::BufLock(CGxBuf* buf) { CGxDevice::BufLock(buf); if (!this->m_context) { return nullptr; } CGxPool* pool = buf->m_pool; if (!pool->m_mem) { pool->m_mem = SMemAlloc(pool->m_size, __FILE__, __LINE__, 0x0); } if (pool->m_usage == GxPoolUsage_Stream) { uint32_t v7 = pool->unk1C + buf->m_itemSize - 1; uint32_t alignedNext = v7 - v7 % buf->m_itemSize; if (alignedNext + buf->m_size > static_cast(pool->m_size)) { pool->Discard(); alignedNext = 0; } buf->m_index = alignedNext; pool->unk1C = alignedNext + buf->m_size; } if (!pool->m_mem) { return nullptr; } return static_cast(pool->m_mem) + buf->m_index; } int32_t CGxDeviceMTL::BufUnlock(CGxBuf* buf, uint32_t size) { CGxDevice::BufUnlock(buf, size); buf->unk1D = 1; return 1; } void CGxDeviceMTL::BufData(CGxBuf* buf, const void* data, size_t size, uintptr_t offset) { CGxDevice::BufData(buf, data, size, offset); auto bufData = this->BufLock(buf); if (bufData) { memcpy(&bufData[offset], data, size); } this->BufUnlock(buf, static_cast(size)); } void CGxDeviceMTL::TexDestroy(CGxTex* texId) { if (texId && texId->m_apiSpecificData) { auto texture = (id)texId->m_apiSpecificData; [texture release]; texId->m_apiSpecificData = nullptr; } if (texId && texId->m_apiSpecificData2) { auto sampler = (id)texId->m_apiSpecificData2; [sampler release]; texId->m_apiSpecificData2 = nullptr; } CGxDevice::TexDestroy(texId); } void CGxDeviceMTL::IShaderCreate(CGxShader* shader) { shader->loaded = 1; shader->valid = 1; } void CGxDeviceMTL::ShaderCreate(CGxShader* shaders[], EGxShTarget target, const char* a4, const char* a5, int32_t permutations) { CGxDevice::ShaderCreate(shaders, target, a4, a5, permutations); } int32_t CGxDeviceMTL::StereoEnabled() { return 0; } void CGxDeviceMTL::XformSetProjection(const C44Matrix& matrix) { CGxDevice::XformSetProjection(matrix); C44Matrix projNative = matrix; if (NotEqual(projNative.c3, 1.0f, WHOA_EPSILON_1) && NotEqual(projNative.c3, 0.0f, WHOA_EPSILON_1)) { projNative = projNative * (1.0f / projNative.c3); } if (projNative.d3 == 0.0f) { auto v5 = -(projNative.d2 / (projNative.c2 + 1.0f)); auto v6 = -(projNative.d2 / (projNative.c2 - 1.0f)); projNative.c2 = v6 / (v6 - v5); projNative.d2 = v6 * v5 / (v5 - v6); } else { auto v8 = 1.0f / projNative.c2; auto v9 = (-1.0f - projNative.d2) * v8; auto v10 = v8 * (1.0f - projNative.d2); projNative.c2 = 1.0f / (v10 - v9); projNative.d2 = v9 / (v9 - v10); } if (!this->MasterEnable(GxMasterEnable_NormalProjection) && projNative.d3 != 1.0f) { C44Matrix shrink = { 0.2f, 0.0f, 0.0f, 0.0f, 0.0f, 0.2f, 0.0f, 0.0f, 0.0f, 0.0f, 0.2f, 0.0f, 0.0f, 0.0f, 0.0f, 1.0f }; projNative = projNative * shrink; } this->m_xforms[GxXform_Projection].m_dirty = 1; this->m_projNative = projNative; } void CGxDeviceMTL::EnsureDepthTexture(uint32_t width, uint32_t height) { if (!this->m_device || (this->m_depthTexture && this->m_depthWidth == width && this->m_depthHeight == height)) { return; } if (this->m_depthTexture) { auto depthTex = (id)this->m_depthTexture; [depthTex release]; this->m_depthTexture = nullptr; } auto device = (id)this->m_device; auto desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatDepth32Float width:width height:height mipmapped:NO]; desc.usage = MTLTextureUsageRenderTarget; desc.storageMode = MTLStorageModePrivate; id depthTex = [device newTextureWithDescriptor:desc]; this->m_depthTexture = depthTex; this->m_depthWidth = width; this->m_depthHeight = height; } void* CGxDeviceMTL::GetDepthState(bool depthTest, bool depthWrite, uint32_t depthFunc) { uint32_t funcIndex = std::min(depthFunc, 3u); auto& cached = this->m_depthStates[depthTest ? 1 : 0][depthWrite ? 1 : 0][funcIndex]; if (cached) { return cached; } auto device = (id)this->m_device; if (!device) { return nullptr; } auto desc = [MTLDepthStencilDescriptor new]; if (!depthTest) { desc.depthCompareFunction = MTLCompareFunctionAlways; } else { switch (funcIndex) { case 0: desc.depthCompareFunction = MTLCompareFunctionLessEqual; break; case 1: desc.depthCompareFunction = MTLCompareFunctionEqual; break; case 2: desc.depthCompareFunction = MTLCompareFunctionGreaterEqual; break; case 3: desc.depthCompareFunction = MTLCompareFunctionLess; break; default: desc.depthCompareFunction = MTLCompareFunctionAlways; break; } } desc.depthWriteEnabled = depthWrite ? YES : NO; id state = [device newDepthStencilStateWithDescriptor:desc]; [desc release]; cached = state; return state; } void CGxDeviceMTL::ITexCreate(CGxTex* texId) { if (!texId || !this->m_device) { return; } EGxTexFormat format = texId->m_dataFormat != GxTex_Unknown ? texId->m_dataFormat : texId->m_format; auto pixelFormat = MtlPixelFormatForGx(format); if (pixelFormat == MTLPixelFormatInvalid) { return; } uint32_t width = 0; uint32_t height = 0; uint32_t baseMip = 0; uint32_t mipCount = 0; this->ITexWHDStartEnd(texId, width, height, baseMip, mipCount); auto device = (id)this->m_device; MTLTextureDescriptor* desc = nil; if (texId->m_target == GxTex_CubeMap) { // Create cubemap texture (6 faces) desc = [MTLTextureDescriptor textureCubeDescriptorWithPixelFormat:pixelFormat size:width mipmapped:(mipCount - baseMip) > 1]; } else { // Create 2D texture desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:(mipCount - baseMip) > 1]; } desc.usage = MTLTextureUsageShaderRead; desc.storageMode = MTLStorageModeShared; id texture = [device newTextureWithDescriptor:desc]; texId->m_apiSpecificData = texture; texId->m_needsCreation = 0; } void CGxDeviceMTL::ITexUpload(CGxTex* texId) { auto texture = (id)texId->m_apiSpecificData; if (!texture) { return; } uint32_t texelStrideInBytes = 0; const void* texels = nullptr; texId->m_userFunc( GxTex_Lock, texId->m_width, texId->m_height, 0, 0, texId->m_userArg, texelStrideInBytes, texels ); uint32_t width = 0; uint32_t height = 0; uint32_t baseMip = 0; uint32_t mipCount = 0; this->ITexWHDStartEnd(texId, width, height, baseMip, mipCount); EGxTexFormat format = texId->m_dataFormat != GxTex_Unknown ? texId->m_dataFormat : texId->m_format; const bool compressed = GxTexIsCompressed(format); // Cubemaps have 6 faces, regular textures have 1 int32_t numFaces = texId->m_target == GxTex_CubeMap ? 6 : 1; for (int32_t face = 0; face < numFaces; ++face) { for (uint32_t mipLevel = baseMip; mipLevel < mipCount; ++mipLevel) { texels = nullptr; texId->m_userFunc( GxTex_Latch, std::max(texId->m_width >> mipLevel, 1u), std::max(texId->m_height >> mipLevel, 1u), face, mipLevel, texId->m_userArg, texelStrideInBytes, texels ); if (!texels) { continue; } uint32_t mipWidth = std::max(texId->m_width >> mipLevel, 1u); uint32_t mipHeight = std::max(texId->m_height >> mipLevel, 1u); MTLRegion region = MTLRegionMake2D(0, 0, mipWidth, mipHeight); if (compressed) { uint32_t blockSize = CGxDevice::s_texFormatBytesPerBlock[format]; uint32_t blocksWide = std::max(1u, (mipWidth + 3) / 4); uint32_t bytesPerRow = blocksWide * blockSize; if (texId->m_target == GxTex_CubeMap) { [texture replaceRegion:region mipmapLevel:mipLevel - baseMip slice:face withBytes:texels bytesPerRow:bytesPerRow bytesPerImage:0]; } else { [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:bytesPerRow]; } } else { const void* uploadTexels = texels; std::vector convertedData; // RGBA8 is 32-bit per pixel // Handle Argb4444 format conversion to RGBA8 // Game packs as: A[15:12] R[11:8] G[7:4] B[3:0] (each 4 bits) // We expand to RGBA8: R[7:0] G[7:0] B[7:0] A[7:0] (each 8 bits) if (format == GxTex_Argb4444) { uint32_t pixelCount = mipWidth * mipHeight; convertedData.resize(pixelCount); const uint16_t* srcData = static_cast(texels); for (uint32_t i = 0; i < pixelCount; ++i) { uint16_t pixel = srcData[i]; // Extract 4-bit nibbles from game's ARGB format uint8_t a4 = (pixel >> 12) & 0xF; uint8_t r4 = (pixel >> 8) & 0xF; uint8_t g4 = (pixel >> 4) & 0xF; uint8_t b4 = pixel & 0xF; // Expand 4-bit to 8-bit (multiply by 17 = 0x11 to scale 0-15 to 0-255) uint8_t r8 = r4 * 17; uint8_t g8 = g4 * 17; uint8_t b8 = b4 * 17; uint8_t a8 = a4 * 17; // Pack as RGBA8 (little-endian: ABGR in memory) convertedData[i] = (a8 << 24) | (b8 << 16) | (g8 << 8) | r8; } uploadTexels = convertedData.data(); texelStrideInBytes = mipWidth * 4; // 4 bytes per pixel for RGBA8 } if (texId->m_target == GxTex_CubeMap) { [texture replaceRegion:region mipmapLevel:mipLevel - baseMip slice:face withBytes:uploadTexels bytesPerRow:texelStrideInBytes bytesPerImage:0]; } else { [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:uploadTexels bytesPerRow:texelStrideInBytes]; } } } } texId->m_userFunc( GxTex_Unlock, texId->m_width, texId->m_height, 0, 0, texId->m_userArg, texelStrideInBytes, texels ); } void* CGxDeviceMTL::GetTexture(CGxTex* texId) { if (!texId) { return nullptr; } if (texId->m_needsCreation || texId->m_needsUpdate || !texId->m_apiSpecificData) { texId->m_needsUpdate = 1; this->ITexMarkAsUpdated(texId); } return texId->m_apiSpecificData; } void* CGxDeviceMTL::GetSampler(CGxTex* texId) { if (!texId) { this->EnsureFallbackTexture(); return this->m_fallbackSampler; } if (!texId->m_apiSpecificData2 || texId->m_needsFlagUpdate) { auto device = (id)this->m_device; if (!device) { return nullptr; } if (texId->m_apiSpecificData2) { auto sampler = (id)texId->m_apiSpecificData2; [sampler release]; texId->m_apiSpecificData2 = nullptr; } auto desc = [MTLSamplerDescriptor new]; desc.sAddressMode = texId->m_flags.m_wrapU ? MTLSamplerAddressModeRepeat : MTLSamplerAddressModeClampToEdge; desc.tAddressMode = texId->m_flags.m_wrapV ? MTLSamplerAddressModeRepeat : MTLSamplerAddressModeClampToEdge; switch (texId->m_flags.m_filter) { case GxTex_Nearest: desc.minFilter = MTLSamplerMinMagFilterNearest; desc.magFilter = MTLSamplerMinMagFilterNearest; desc.mipFilter = MTLSamplerMipFilterNotMipmapped; break; case GxTex_NearestMipNearest: desc.minFilter = MTLSamplerMinMagFilterNearest; desc.magFilter = MTLSamplerMinMagFilterNearest; desc.mipFilter = MTLSamplerMipFilterNearest; break; case GxTex_LinearMipNearest: desc.minFilter = MTLSamplerMinMagFilterLinear; desc.magFilter = MTLSamplerMinMagFilterLinear; desc.mipFilter = MTLSamplerMipFilterNearest; break; case GxTex_LinearMipLinear: desc.minFilter = MTLSamplerMinMagFilterLinear; desc.magFilter = MTLSamplerMinMagFilterLinear; desc.mipFilter = MTLSamplerMipFilterLinear; break; case GxTex_Anisotropic: desc.minFilter = MTLSamplerMinMagFilterLinear; desc.magFilter = MTLSamplerMinMagFilterLinear; desc.mipFilter = MTLSamplerMipFilterLinear; desc.maxAnisotropy = std::max(texId->m_flags.m_maxAnisotropy, 1); break; default: desc.minFilter = MTLSamplerMinMagFilterLinear; desc.magFilter = MTLSamplerMinMagFilterLinear; desc.mipFilter = MTLSamplerMipFilterNotMipmapped; break; } id sampler = [device newSamplerStateWithDescriptor:desc]; [desc release]; texId->m_apiSpecificData2 = sampler; texId->m_needsFlagUpdate = 0; } return texId->m_apiSpecificData2; } void CGxDeviceMTL::EnsureFallbackTexture() { if (this->m_fallbackTexture || !this->m_device) { return; } auto device = (id)this->m_device; auto desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm width:1 height:1 mipmapped:NO]; desc.usage = MTLTextureUsageShaderRead; desc.storageMode = MTLStorageModeShared; id texture = [device newTextureWithDescriptor:desc]; uint32_t pixel = 0xFFFFFFFF; MTLRegion region = MTLRegionMake2D(0, 0, 1, 1); [texture replaceRegion:region mipmapLevel:0 withBytes:&pixel bytesPerRow:4]; auto samplerDesc = [MTLSamplerDescriptor new]; samplerDesc.sAddressMode = MTLSamplerAddressModeClampToEdge; samplerDesc.tAddressMode = MTLSamplerAddressModeClampToEdge; samplerDesc.minFilter = MTLSamplerMinMagFilterLinear; samplerDesc.magFilter = MTLSamplerMinMagFilterLinear; samplerDesc.mipFilter = MTLSamplerMipFilterNotMipmapped; id sampler = [device newSamplerStateWithDescriptor:samplerDesc]; [samplerDesc release]; this->m_fallbackTexture = texture; this->m_fallbackSampler = sampler; } void CGxDeviceMTL::Resize(uint32_t width, uint32_t height) { CRect rect = { 0.0f, 0.0f, static_cast(height), static_cast(width) }; this->DeviceSetDefWindow(rect); }