From a9cad5238d84d9ac6c9da79b7e194d63daa36f6e Mon Sep 17 00:00:00 2001 From: aomizu Date: Tue, 23 Dec 2025 12:42:14 +0900 Subject: [PATCH 1/8] init metal backend --- src/app/CMakeLists.txt | 2 + src/app/mac/EngineMTLLayerView.h | 9 + src/app/mac/EngineMTLLayerView.mm | 36 ++++ src/app/mac/View.mm | 6 + src/app/mac/WindowCallbacks.mm | 7 +- src/console/Device.cpp | 8 + src/gx/CGxDevice.cpp | 8 + src/gx/CGxDevice.hpp | 1 + src/gx/CMakeLists.txt | 1 + src/gx/Device.cpp | 2 + src/gx/Types.hpp | 3 +- src/gx/mtl/CGxDeviceMTL.hpp | 49 +++++ src/gx/mtl/CGxDeviceMTL.mm | 323 ++++++++++++++++++++++++++++++ test/CMakeLists.txt | 2 + 14 files changed, 455 insertions(+), 2 deletions(-) create mode 100644 src/app/mac/EngineMTLLayerView.h create mode 100644 src/app/mac/EngineMTLLayerView.mm create mode 100644 src/gx/mtl/CGxDeviceMTL.hpp create mode 100644 src/gx/mtl/CGxDeviceMTL.mm diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a829594..33a6a5d 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -32,6 +32,8 @@ if(WHOA_SYSTEM_MAC) "-framework AppKit" "-framework Carbon" "-framework IOKit" + "-framework Metal" + "-framework QuartzCore" ) install(DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/mac/MainMenu.nib DESTINATION "bin") diff --git a/src/app/mac/EngineMTLLayerView.h b/src/app/mac/EngineMTLLayerView.h new file mode 100644 index 0000000..21c99a7 --- /dev/null +++ b/src/app/mac/EngineMTLLayerView.h @@ -0,0 +1,9 @@ +#ifndef APP_MAC_ENGINE_MTL_LAYER_VIEW_H +#define APP_MAC_ENGINE_MTL_LAYER_VIEW_H + +#include "app/mac/EngineGLLayerView.h" + +@interface EngineMTLLayerView : EngineGLLayerView +@end + +#endif diff --git a/src/app/mac/EngineMTLLayerView.mm b/src/app/mac/EngineMTLLayerView.mm new file mode 100644 index 0000000..590d69f --- /dev/null +++ b/src/app/mac/EngineMTLLayerView.mm @@ -0,0 +1,36 @@ +#include "app/mac/EngineMTLLayerView.h" +#import + +@implementation EngineMTLLayerView + +- (CALayer*)makeBackingLayer { + return [CAMetalLayer layer]; +} + +- (id)initWithFrame:(NSRect)frame glWindow:(GLWindow*)window { + self = [super initWithFrame:frame glWindow:window]; + + if (self) { + [self setWantsLayer:YES]; + } + + return self; +} + +- (void)drawRect:(NSRect)dirtyRect { + // Rendering is driven by CGxDeviceMTL. +} + +- (void)update { + [super update]; + + if (![self.layer isKindOfClass:[CAMetalLayer class]]) { + return; + } + + CAMetalLayer* layer = (CAMetalLayer*)self.layer; + CGSize size = [self convertSizeToBacking:self.bounds.size]; + layer.drawableSize = size; +} + +@end diff --git a/src/app/mac/View.mm b/src/app/mac/View.mm index 729115b..a7c8fbb 100644 --- a/src/app/mac/View.mm +++ b/src/app/mac/View.mm @@ -1,7 +1,9 @@ #include "app/mac/View.h" #include "app/mac/EngineGLLayerView.h" +#include "app/mac/EngineMTLLayerView.h" #include "app/mac/WindowCallbacks.h" #include "gx/gll/GLWindow.h" +#include "gx/Device.hpp" GLWindowCallbacks EngineViewCallbacks = { &MacOnResized, @@ -23,5 +25,9 @@ void AssignEngineViewCallbacks(GLWindowCallbacks* callbacks) { } Class GetEngineViewClass() { + if (GxDevApi() == GxApi_Metal) { + return [EngineMTLLayerView class]; + } + return [EngineGLLayerView class]; } diff --git a/src/app/mac/WindowCallbacks.mm b/src/app/mac/WindowCallbacks.mm index ccdd816..841becc 100644 --- a/src/app/mac/WindowCallbacks.mm +++ b/src/app/mac/WindowCallbacks.mm @@ -2,6 +2,7 @@ #include "app/mac/MacClient.h" #include "event/Input.hpp" #include "gx/gll/CGxDeviceGLL.hpp" +#include "gx/mtl/CGxDeviceMTL.hpp" #include "gx/Device.hpp" #include "gx/Window.hpp" #include @@ -171,7 +172,11 @@ void MacOnResized(int32_t width, int32_t height, bool a3) { return; } - static_cast(g_theGxDevicePtr)->Resize(width, height); + if (GxDevApi() == GxApi_GLL) { + static_cast(g_theGxDevicePtr)->Resize(width, height); + } else if (GxDevApi() == GxApi_Metal) { + static_cast(g_theGxDevicePtr)->Resize(width, height); + } OsQueuePut(OS_INPUT_SIZE, width, height, 0, 0); diff --git a/src/console/Device.cpp b/src/console/Device.cpp index dcfbb12..5178595 100644 --- a/src/console/Device.cpp +++ b/src/console/Device.cpp @@ -6,6 +6,7 @@ #include "gx/Adapter.hpp" #include "gx/Device.hpp" #include +#include #include static CGxDevice* s_device; @@ -417,6 +418,13 @@ void ConsoleDeviceInitialize(const char* title) { api = GxApi_GLL; #endif +#if defined(WHOA_SYSTEM_MAC) + const char* apiOverride = getenv("WHOA_GX_API"); + if (apiOverride && !strcmp(apiOverride, "metal")) { + api = GxApi_Metal; + } +#endif + s_device = GxDevCreate(api, OsWindowProc, format); // TODO diff --git a/src/gx/CGxDevice.cpp b/src/gx/CGxDevice.cpp index 880a107..c0fa16d 100644 --- a/src/gx/CGxDevice.cpp +++ b/src/gx/CGxDevice.cpp @@ -18,6 +18,7 @@ #if defined(WHOA_SYSTEM_MAC) #include "gx/gll/CGxDeviceGLL.hpp" + #include "gx/mtl/CGxDeviceMTL.hpp" #include "gx/mac/Display.hpp" #include #include @@ -117,6 +118,8 @@ int32_t CGxDevice::AdapterFormats(EGxApi api, TSGrowableArray& adapte CGxDevice::OpenGlAdapterFormats(adapterFormats); } else if (api == GxApi_GLL) { CGxDevice::GLLAdapterFormats(adapterFormats); + } else if (api == GxApi_Metal) { + CGxDevice::OpenGlAdapterFormats(adapterFormats); } #elif defined(WHOA_SYSTEM_LINUX) @@ -228,6 +231,11 @@ CGxDevice* CGxDevice::NewGLL() { auto m = SMemAlloc(sizeof(CGxDeviceGLL), __FILE__, __LINE__, 0x0); return new (m) CGxDeviceGLL(); } + +CGxDevice* CGxDevice::NewMTL() { + auto m = SMemAlloc(sizeof(CGxDeviceMTL), __FILE__, __LINE__, 0x0); + return new (m) CGxDeviceMTL(); +} #endif CGxDevice* CGxDevice::NewOpenGl() { diff --git a/src/gx/CGxDevice.hpp b/src/gx/CGxDevice.hpp index de72727..c0cd76e 100644 --- a/src/gx/CGxDevice.hpp +++ b/src/gx/CGxDevice.hpp @@ -70,6 +70,7 @@ class CGxDevice { #endif #if defined(WHOA_SYSTEM_MAC) static CGxDevice* NewGLL(); + static CGxDevice* NewMTL(); #endif static CGxDevice* NewOpenGl(); static void OpenGlAdapterFormats(TSGrowableArray& adapterFormats); diff --git a/src/gx/CMakeLists.txt b/src/gx/CMakeLists.txt index a9c1ede..3bd2abd 100644 --- a/src/gx/CMakeLists.txt +++ b/src/gx/CMakeLists.txt @@ -20,6 +20,7 @@ if(WHOA_SYSTEM_MAC) file(GLOB MAC_SOURCES "gll/*.cpp" "gll/*.mm" + "mtl/*.mm" "mac/*.cpp" ) diff --git a/src/gx/Device.cpp b/src/gx/Device.cpp index d30db2c..2cedd26 100644 --- a/src/gx/Device.cpp +++ b/src/gx/Device.cpp @@ -24,6 +24,8 @@ CGxDevice* GxDevCreate(EGxApi api, int32_t (*windowProc)(void* window, uint32_t device = CGxDevice::NewOpenGl(); } else if (api == GxApi_GLL) { device = CGxDevice::NewGLL(); + } else if (api == GxApi_Metal) { + device = CGxDevice::NewMTL(); } else { // Error } diff --git a/src/gx/Types.hpp b/src/gx/Types.hpp index 9ceaf2d..438b051 100644 --- a/src/gx/Types.hpp +++ b/src/gx/Types.hpp @@ -35,7 +35,8 @@ enum EGxApi { GxApi_D3d10 = 3, GxApi_D3d11 = 4, GxApi_GLL = 5, - GxApis_Last = 6 + GxApi_Metal = 6, + GxApis_Last = 7 }; enum EGxBlend { diff --git a/src/gx/mtl/CGxDeviceMTL.hpp b/src/gx/mtl/CGxDeviceMTL.hpp new file mode 100644 index 0000000..a9f6be0 --- /dev/null +++ b/src/gx/mtl/CGxDeviceMTL.hpp @@ -0,0 +1,49 @@ +#ifndef GX_MTL_C_GX_DEVICE_MTL_HPP +#define GX_MTL_C_GX_DEVICE_MTL_HPP + +#include "gx/CGxDevice.hpp" +#include "gx/gll/GLWindow.h" + +class CGxBatch; +class CGxShader; + +class CGxDeviceMTL : public CGxDevice { + public: + // Member variables + GLWindow m_window; + + // Virtual member functions + void ITexMarkAsUpdated(CGxTex*) override; + void IRsSendToHw(EGxRenderState) override; + int32_t DeviceCreate(int32_t (*windowProc)(void* window, uint32_t message, uintptr_t wparam, intptr_t lparam), const CGxFormat&) override; + int32_t DeviceSetFormat(const CGxFormat&) override; + void* DeviceWindow() override; + void DeviceWM(EGxWM wm, uintptr_t param1, uintptr_t param2) override {}; + void CapsWindowSize(CRect&) override; + void CapsWindowSizeInScreenCoords(CRect& dst) override; + void ScenePresent() override; + void SceneClear(uint32_t, CImVector) override; + void Draw(CGxBatch* batch, int32_t indexed) override; + void PoolSizeSet(CGxPool*, uint32_t) override; + char* BufLock(CGxBuf*) override; + int32_t BufUnlock(CGxBuf*, uint32_t) override; + void BufData(CGxBuf* buf, const void* data, size_t size, uintptr_t offset) override; + void TexDestroy(CGxTex* texId) override; + void IShaderCreate(CGxShader*) override; + void ShaderCreate(CGxShader*[], EGxShTarget, const char*, const char*, int32_t) override; + int32_t StereoEnabled(void) override; + + // Member functions + CGxDeviceMTL(); + void Resize(uint32_t width, uint32_t height); + + private: + void ISetCaps(const CGxFormat& format); + void* m_device = nullptr; + void* m_commandQueue = nullptr; + void* m_layer = nullptr; + uint32_t m_clearMask = 0; + uint32_t m_clearColor = 0; +}; + +#endif diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm new file mode 100644 index 0000000..13c13e6 --- /dev/null +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -0,0 +1,323 @@ +#include "gx/mtl/CGxDeviceMTL.hpp" +#include "app/mac/View.h" +#include "gx/Window.hpp" +#include "util/Autorelease.hpp" +#include +#include +#include + +#import +#import +#import + +CGxDeviceMTL::CGxDeviceMTL() : CGxDevice() { + this->m_api = GxApi_Metal; + this->m_caps.m_colorFormat = GxCF_rgba; + + this->DeviceCreatePools(); + this->DeviceCreateStreamBufs(); +} + +void CGxDeviceMTL::ITexMarkAsUpdated(CGxTex* texId) { + CGxDevice::ITexMarkAsUpdated(texId); +} + +void CGxDeviceMTL::IRsSendToHw(EGxRenderState which) { + (void)which; +} + +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_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::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; + } + + auto device = (id)this->m_device; + auto commandQueue = (id)this->m_commandQueue; + auto layer = (CAMetalLayer*)this->m_layer; + + if (!device || !commandQueue || !layer) { + return; + } + + System_Autorelease::ScopedPool autorelease; + + id drawable = [layer nextDrawable]; + if (!drawable) { + return; + } + + 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 = (this->m_clearMask & 0x1) ? MTLLoadActionClear : MTLLoadActionLoad; + pass.colorAttachments[0].storeAction = MTLStoreActionStore; + pass.colorAttachments[0].clearColor = MTLClearColorMake( + r / 255.0f, + g / 255.0f, + b / 255.0f, + a / 255.0f + ); + + id commandBuffer = [commandQueue commandBuffer]; + id encoder = [commandBuffer renderCommandEncoderWithDescriptor: pass]; + [encoder endEncoding]; + + [commandBuffer presentDrawable: drawable]; + [commandBuffer commit]; +} + +void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { + (void)batch; + (void)indexed; +} + +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_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) { + 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::Resize(uint32_t width, uint32_t height) { + CRect rect = { 0.0f, 0.0f, static_cast(height), static_cast(width) }; + this->DeviceSetDefWindow(rect); +} diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index a2292d6..9ddef9b 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -22,6 +22,8 @@ if(WHOA_SYSTEM_MAC) "-framework AppKit" "-framework Carbon" "-framework IOKit" + "-framework Metal" + "-framework QuartzCore" ) endif() From 81970958a8de38d6b9b627d6e90dba21f1f50412 Mon Sep 17 00:00:00 2001 From: aomizu Date: Tue, 23 Dec 2025 13:09:50 +0900 Subject: [PATCH 2/8] feat: Add debug rendering pipeline to draw a triangle in the Metal backend. --- src/gx/mtl/CGxDeviceMTL.hpp | 4 ++ src/gx/mtl/CGxDeviceMTL.mm | 108 ++++++++++++++++++++++++++++++++++-- 2 files changed, 107 insertions(+), 5 deletions(-) diff --git a/src/gx/mtl/CGxDeviceMTL.hpp b/src/gx/mtl/CGxDeviceMTL.hpp index a9f6be0..0410514 100644 --- a/src/gx/mtl/CGxDeviceMTL.hpp +++ b/src/gx/mtl/CGxDeviceMTL.hpp @@ -39,9 +39,13 @@ class CGxDeviceMTL : public CGxDevice { private: void ISetCaps(const CGxFormat& format); + void EnsureDebugPipeline(); void* m_device = nullptr; void* m_commandQueue = nullptr; void* m_layer = nullptr; + void* m_pipeline = nullptr; + void* m_vertexBuffer = nullptr; + uint32_t m_vertexCount = 0; uint32_t m_clearMask = 0; uint32_t m_clearColor = 0; }; diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index 13c13e6..7f08a97 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -10,6 +10,40 @@ #import #import +namespace { + struct DebugVertex { + float position[2]; + float color[4]; + }; + + const DebugVertex kDebugTriangle[] = { + { { -0.6f, -0.6f }, { 1.0f, 0.2f, 0.2f, 1.0f } }, + { { 0.0f, 0.6f }, { 0.2f, 1.0f, 0.2f, 1.0f } }, + { { 0.6f, -0.6f }, { 0.2f, 0.2f, 1.0f, 1.0f } } + }; + + const char kDebugShaderSource[] = + "#include \n" + "using namespace metal;\n" + "struct VertexIn {\n" + " packed_float2 position;\n" + " packed_float4 color;\n" + "};\n" + "struct VertexOut {\n" + " float4 position [[position]];\n" + " float4 color;\n" + "};\n" + "vertex VertexOut vs_main(uint vid [[vertex_id]], const device VertexIn* v [[buffer(0)]]) {\n" + " VertexOut out;\n" + " out.position = float4(v[vid].position, 0.0, 1.0);\n" + " out.color = v[vid].color;\n" + " return out;\n" + "}\n" + "fragment float4 ps_main(VertexOut in [[stage_in]]) {\n" + " return in.color;\n" + "}\n"; +} + CGxDeviceMTL::CGxDeviceMTL() : CGxDevice() { this->m_api = GxApi_Metal; this->m_caps.m_colorFormat = GxCF_rgba; @@ -151,6 +185,63 @@ int32_t CGxDeviceMTL::DeviceSetFormat(const CGxFormat& format) { return 1; } +void CGxDeviceMTL::EnsureDebugPipeline() { + if (this->m_pipeline || !this->m_device || !this->m_layer) { + return; + } + + auto device = (id)this->m_device; + auto layer = (CAMetalLayer*)this->m_layer; + + NSString* source = [[NSString alloc] initWithUTF8String:kDebugShaderSource]; + NSError* error = nil; + id library = [device newLibraryWithSource:source options:nil error:&error]; + [source release]; + + if (!library) { + return; + } + + id vs = [library newFunctionWithName:@"vs_main"]; + id ps = [library newFunctionWithName:@"ps_main"]; + if (!vs || !ps) { + if (vs) { + [vs release]; + } + if (ps) { + [ps release]; + } + [library release]; + return; + } + + MTLRenderPipelineDescriptor* desc = [MTLRenderPipelineDescriptor new]; + desc.vertexFunction = vs; + desc.fragmentFunction = ps; + desc.colorAttachments[0].pixelFormat = layer.pixelFormat; + + id pipeline = [device newRenderPipelineStateWithDescriptor:desc error:&error]; + [desc release]; + [vs release]; + [ps release]; + [library release]; + + if (!pipeline) { + return; + } + + id buffer = [device newBufferWithBytes:kDebugTriangle + length:sizeof(kDebugTriangle) + options:MTLResourceStorageModeShared]; + if (!buffer) { + return; + } + + this->m_pipeline = pipeline; + this->m_vertexBuffer = buffer; + this->m_vertexCount = static_cast(sizeof(kDebugTriangle) / sizeof(kDebugTriangle[0])); +} + void* CGxDeviceMTL::DeviceWindow() { return &this->m_window; } @@ -204,6 +295,8 @@ void CGxDeviceMTL::ScenePresent() { System_Autorelease::ScopedPool autorelease; + this->EnsureDebugPipeline(); + id drawable = [layer nextDrawable]; if (!drawable) { return; @@ -216,17 +309,22 @@ void CGxDeviceMTL::ScenePresent() { auto pass = [MTLRenderPassDescriptor renderPassDescriptor]; pass.colorAttachments[0].texture = drawable.texture; - pass.colorAttachments[0].loadAction = (this->m_clearMask & 0x1) ? MTLLoadActionClear : MTLLoadActionLoad; + pass.colorAttachments[0].loadAction = MTLLoadActionClear; pass.colorAttachments[0].storeAction = MTLStoreActionStore; pass.colorAttachments[0].clearColor = MTLClearColorMake( - r / 255.0f, - g / 255.0f, - b / 255.0f, - a / 255.0f + (this->m_clearMask & 0x1) ? r / 255.0f : 0.0f, + (this->m_clearMask & 0x1) ? g / 255.0f : 0.0f, + (this->m_clearMask & 0x1) ? b / 255.0f : 0.0f, + (this->m_clearMask & 0x1) ? a / 255.0f : 1.0f ); id commandBuffer = [commandQueue commandBuffer]; id encoder = [commandBuffer renderCommandEncoderWithDescriptor: pass]; + if (encoder && this->m_pipeline && this->m_vertexBuffer) { + [encoder setRenderPipelineState:(id)this->m_pipeline]; + [encoder setVertexBuffer:(id)this->m_vertexBuffer offset:0 atIndex:0]; + [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:this->m_vertexCount]; + } [encoder endEncoding]; [commandBuffer presentDrawable: drawable]; From 1ad3679f906eb10682c4f43ac1e159f9508f510f Mon Sep 17 00:00:00 2001 From: aomizu Date: Tue, 23 Dec 2025 17:50:14 +0900 Subject: [PATCH 3/8] feat: Implement initial Metal graphics device with comprehensive shader system and pipeline management. --- src/client/Client.cpp | 2 + src/gx/mtl/CGxDeviceMTL.hpp | 36 +- src/gx/mtl/CGxDeviceMTL.mm | 1240 ++++++++++++++++++++++++++++++++--- 3 files changed, 1201 insertions(+), 77 deletions(-) diff --git a/src/client/Client.cpp b/src/client/Client.cpp index 8863218..f563387 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -11,6 +11,7 @@ #include "glue/CGlueMgr.hpp" #include "glue/GlueScriptEvents.hpp" #include "gx/Screen.hpp" +#include "gx/Shader.hpp" #include "gx/Texture.hpp" #include "model/Model2.hpp" #include "net/Poll.hpp" @@ -235,6 +236,7 @@ int32_t InitializeEngineCallback(const void* a1, void* a2) { auto m2Flags = M2RegisterCVars(); M2Initialize(m2Flags, 0); + CShaderEffect::InitShaderSystem((M2GetCacheFlags() & 0x8) != 0, 0); // v4 = *(_DWORD *)(dword_B2FA00 + 48); // sub_4B61C0(dword_AB6128[v4]); diff --git a/src/gx/mtl/CGxDeviceMTL.hpp b/src/gx/mtl/CGxDeviceMTL.hpp index 0410514..6a02b77 100644 --- a/src/gx/mtl/CGxDeviceMTL.hpp +++ b/src/gx/mtl/CGxDeviceMTL.hpp @@ -32,6 +32,7 @@ class CGxDeviceMTL : public CGxDevice { void IShaderCreate(CGxShader*) override; void ShaderCreate(CGxShader*[], EGxShTarget, const char*, const char*, int32_t) override; int32_t StereoEnabled(void) override; + void XformSetProjection(const C44Matrix& matrix) override; // Member functions CGxDeviceMTL(); @@ -39,15 +40,42 @@ class CGxDeviceMTL : public CGxDevice { private: void ISetCaps(const CGxFormat& format); - void EnsureDebugPipeline(); + void EnsureLibrary(); + void BeginFrame(); + void* GetPipeline(EGxVertexBufferFormat format, bool useColor, bool useSkin, bool useTex, int32_t blendMode); + void* GetPoolBuffer(CGxPool* pool); + void ITexCreate(CGxTex* texId); + void ITexUpload(CGxTex* texId); + void* GetTexture(CGxTex* texId); + void* GetSampler(CGxTex* texId); + void EnsureFallbackTexture(); + void EnsureDepthTexture(uint32_t width, uint32_t height); + void* GetDepthState(bool depthTest, bool depthWrite, uint32_t depthFunc); void* m_device = nullptr; void* m_commandQueue = nullptr; void* m_layer = nullptr; - void* m_pipeline = nullptr; - void* m_vertexBuffer = nullptr; - uint32_t m_vertexCount = 0; + void* m_shaderLibrary = nullptr; + void* m_pipelineColor[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSolid[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSkin[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineColorTex[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSolidTex[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSkinTex[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineColorTex2[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSolidTex2[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_pipelineSkinTex2[GxVertexBufferFormats_Last][GxBlends_Last] = {}; + void* m_frameCommandBuffer = nullptr; + void* m_frameEncoder = nullptr; + void* m_frameDrawable = nullptr; + uint32_t m_frameHasDraw = 0; uint32_t m_clearMask = 0; uint32_t m_clearColor = 0; + void* m_fallbackTexture = nullptr; + void* m_fallbackSampler = nullptr; + void* m_depthTexture = nullptr; + uint32_t m_depthWidth = 0; + uint32_t m_depthHeight = 0; + void* m_depthStates[2][2][4] = {}; }; #endif diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index 7f08a97..9f98719 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -1,9 +1,16 @@ #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 #import @@ -11,36 +18,339 @@ #import namespace { - struct DebugVertex { - float position[2]; - float color[4]; - }; + 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; + } + } - const DebugVertex kDebugTriangle[] = { - { { -0.6f, -0.6f }, { 1.0f, 0.2f, 0.2f, 1.0f } }, - { { 0.0f, 0.6f }, { 0.2f, 1.0f, 0.2f, 1.0f } }, - { { 0.6f, -0.6f }, { 0.2f, 0.2f, 1.0f, 1.0f } } - }; + bool GxTexIsCompressed(EGxTexFormat format) { + return format == GxTex_Dxt1 || format == GxTex_Dxt3 || format == GxTex_Dxt5; + } - const char kDebugShaderSource[] = + MTLPixelFormat MtlPixelFormatForGx(EGxTexFormat format) { + switch (format) { + case GxTex_Abgr8888: + return MTLPixelFormatRGBA8Unorm; + case GxTex_Argb8888: + return MTLPixelFormatBGRA8Unorm; + case GxTex_Argb4444: + return MTLPixelFormatABGR4Unorm; + 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 VertexIn {\n" - " packed_float2 position;\n" - " packed_float4 color;\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" " float4 color;\n" + " float2 texcoord;\n" + " float2 texcoord1;\n" "};\n" - "vertex VertexOut vs_main(uint vid [[vertex_id]], const device VertexIn* v [[buffer(0)]]) {\n" + "struct PSConstants {\n" + " float alphaRef;\n" + " float4 color;\n" + "};\n" + "struct VSConstants {\n" + " float4x4 mvp;\n" + "};\n" + "vertex VertexOut vs_color(VertexColorIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" " VertexOut out;\n" - " out.position = float4(v[vid].position, 0.0, 1.0);\n" - " out.color = v[vid].color;\n" + " out.position = c.mvp * float4(in.position, 1.0);\n" + " out.color = in.color;\n" + " out.texcoord = float2(0.0, 0.0);\n" + " out.texcoord1 = float2(0.0, 0.0);\n" " return out;\n" "}\n" - "fragment float4 ps_main(VertexOut in [[stage_in]]) {\n" - " return in.color;\n" + "vertex VertexOut vs_tex(VertexTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = c.mvp * float4(in.position, 1.0);\n" + " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord;\n" + " return out;\n" + "}\n" + "vertex VertexOut vs_tex2(VertexTex2In in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = c.mvp * float4(in.position, 1.0);\n" + " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord1;\n" + " return out;\n" + "}\n" + "vertex VertexOut vs_color_tex(VertexColorTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = c.mvp * float4(in.position, 1.0);\n" + " out.color = in.color;\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord;\n" + " return out;\n" + "}\n" + "vertex VertexOut vs_color_tex2(VertexColorTex2In in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = c.mvp * float4(in.position, 1.0);\n" + " out.color = in.color;\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord1;\n" + " return out;\n" + "}\n" + "vertex VertexOut vs_solid(VertexSolidIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" + " VertexOut out;\n" + " out.position = c.mvp * float4(in.position, 1.0);\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" + " 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" + " out.position = c.mvp * float4(skinned, 1.0);\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" + " 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" + " out.position = c.mvp * float4(skinned, 1.0);\n" + " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord;\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" + " out.position = c.mvp * float4(skinned, 1.0);\n" + " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" + " out.texcoord = in.texcoord;\n" + " out.texcoord1 = in.texcoord1;\n" + " return out;\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 color;\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 color;\n" "}\n"; } @@ -53,7 +363,23 @@ CGxDeviceMTL::CGxDeviceMTL() : CGxDevice() { } void CGxDeviceMTL::ITexMarkAsUpdated(CGxTex* texId) { - CGxDevice::ITexMarkAsUpdated(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) { @@ -163,6 +489,9 @@ void CGxDeviceMTL::ISetCaps(const CGxFormat& format) { 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; @@ -185,15 +514,14 @@ int32_t CGxDeviceMTL::DeviceSetFormat(const CGxFormat& format) { return 1; } -void CGxDeviceMTL::EnsureDebugPipeline() { - if (this->m_pipeline || !this->m_device || !this->m_layer) { +void CGxDeviceMTL::EnsureLibrary() { + if (this->m_shaderLibrary || !this->m_device) { return; } auto device = (id)this->m_device; - auto layer = (CAMetalLayer*)this->m_layer; - NSString* source = [[NSString alloc] initWithUTF8String:kDebugShaderSource]; + NSString* source = [[NSString alloc] initWithUTF8String:kMetalShaderSource]; NSError* error = nil; id library = [device newLibraryWithSource:source options:nil error:&error]; [source release]; @@ -202,8 +530,128 @@ void CGxDeviceMTL::EnsureDebugPipeline() { return; } - id vs = [library newFunctionWithName:@"vs_main"]; - id ps = [library newFunctionWithName:@"ps_main"]; + 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"; + } + } + + psName = useTexPipeline ? @"ps_tex" : @"ps_main"; + + id vs = [library newFunctionWithName:vsName]; + id ps = [library newFunctionWithName:psName]; + if (!vs || !ps) { if (vs) { [vs release]; @@ -211,35 +659,176 @@ void CGxDeviceMTL::EnsureDebugPipeline() { if (ps) { [ps release]; } - [library release]; - return; + return nullptr; } - MTLRenderPipelineDescriptor* desc = [MTLRenderPipelineDescriptor new]; + 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; + } + + NSError* error = nil; id pipeline = [device newRenderPipelineStateWithDescriptor:desc error:&error]; [desc release]; [vs release]; [ps release]; - [library 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; } - id buffer = [device newBufferWithBytes:kDebugTriangle - length:sizeof(kDebugTriangle) - options:MTLResourceStorageModeShared]; - if (!buffer) { + auto commandQueue = (id)this->m_commandQueue; + auto layer = (CAMetalLayer*)this->m_layer; + + id drawable = [layer nextDrawable]; + if (!drawable) { return; } - this->m_pipeline = pipeline; - this->m_vertexBuffer = buffer; - this->m_vertexCount = static_cast(sizeof(kDebugTriangle) / sizeof(kDebugTriangle[0])); + 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() { @@ -285,55 +874,235 @@ void CGxDeviceMTL::ScenePresent() { return; } - auto device = (id)this->m_device; - auto commandQueue = (id)this->m_commandQueue; - auto layer = (CAMetalLayer*)this->m_layer; - - if (!device || !commandQueue || !layer) { - return; - } - System_Autorelease::ScopedPool autorelease; - this->EnsureDebugPipeline(); + if (!this->m_frameEncoder) { + this->BeginFrame(); + } - id drawable = [layer nextDrawable]; - if (!drawable) { + if (!this->m_frameEncoder || !this->m_frameCommandBuffer || !this->m_frameDrawable) { return; } - 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( - (this->m_clearMask & 0x1) ? r / 255.0f : 0.0f, - (this->m_clearMask & 0x1) ? g / 255.0f : 0.0f, - (this->m_clearMask & 0x1) ? b / 255.0f : 0.0f, - (this->m_clearMask & 0x1) ? a / 255.0f : 1.0f - ); - - id commandBuffer = [commandQueue commandBuffer]; - id encoder = [commandBuffer renderCommandEncoderWithDescriptor: pass]; - if (encoder && this->m_pipeline && this->m_vertexBuffer) { - [encoder setRenderPipelineState:(id)this->m_pipeline]; - [encoder setVertexBuffer:(id)this->m_vertexBuffer offset:0 atIndex:0]; - [encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:this->m_vertexCount]; + auto encoder = (id)this->m_frameEncoder; + if (this->intF6C) { + auto drawable = (id)this->m_frameDrawable; + ApplyViewport(this, encoder, drawable.texture); } - [encoder endEncoding]; + auto commandBuffer = (id)this->m_frameCommandBuffer; + auto drawable = (id)this->m_frameDrawable; - [commandBuffer presentDrawable: drawable]; + [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) { - (void)batch; - (void)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; + } + + bool useColor = (this->m_primVertexMask & GxPrim_Color0) != 0; + bool useSkin = this->m_primVertexFormat == GxVBF_PBNT2; + bool useTex = false; + 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; + } + } + } + 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; + } + + C44Matrix mvp; + bool useShaderMvp = false; + + // TODO: Restore shader constant usage once verified. + // For now, we debug log if a vertex shader is present to analyze the constants. + 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); + } + } + } + + if (!useShaderMvp) { + if (useSkin) { + mvp = this->m_projNative; + } else { + const auto& world = this->m_xforms[GxXform_World].TopConst(); + const auto& view = this->m_xforms[GxXform_View].TopConst(); + mvp = (world * view) * this->m_projNative; + } + } + + [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]; + } + + int32_t cullMode = static_cast(this->m_appRenderStates[GxRs_Culling].m_value); + if (cullMode == 0) { + [encoder setCullMode:MTLCullModeNone]; + } else { + [encoder setCullMode:MTLCullModeBack]; + [encoder setFrontFacingWinding:(cullMode == 1) ? MTLWindingClockwise : MTLWindingCounterClockwise]; + } + + [encoder setVertexBuffer:mtlVertexBuf offset:vertexBuf->m_index atIndex:0]; + [encoder setVertexBytes:&mvp length:sizeof(mvp) atIndex:1]; + if (useSkin) { + [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; + } + [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 pad[3]; + float color[4]; + } psConsts; + + psConsts.alphaRef = static_cast(static_cast(this->m_appRenderStates[GxRs_AlphaRef].m_value)) / 255.0f; + psConsts.pad[0] = psConsts.pad[1] = psConsts.pad[2] = 0.0f; + + // Default 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]; + // Sanity check: Only apply if values are within a reasonable range for color/alpha. + // This filters out uninitialized constants (often FLT_MAX) or non-color data. + if (std::abs(c.x) <= 10.0f && std::abs(c.y) <= 10.0f && std::abs(c.z) <= 10.0f && std::abs(c.w) <= 10.0f) { + psConsts.color[0] = c.x; + psConsts.color[1] = c.y; + psConsts.color[2] = c.z; + psConsts.color[3] = c.w; + } + } + } + + [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) { @@ -344,6 +1113,12 @@ void CGxDeviceMTL::PoolSizeSet(CGxPool* pool, uint32_t size) { 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; @@ -399,6 +1174,16 @@ void CGxDeviceMTL::BufData(CGxBuf* buf, const void* data, size_t size, uintptr_t } 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); } @@ -415,6 +1200,315 @@ 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; + auto 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); + + 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), + 0, + 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; + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:bytesPerRow]; + } else { + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels 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); From 15eafe92d703cbbc6b3693031f7948cad1c07c07 Mon Sep 17 00:00:00 2001 From: aomizu Date: Wed, 24 Dec 2025 10:07:13 +0900 Subject: [PATCH 4/8] feat: Implement fog and point size in Metal shaders and refine render state processing for textures and other states. --- src/gx/mtl/CGxDeviceMTL.mm | 379 ++++++++++++++++++++++++++++++++----- 1 file changed, 327 insertions(+), 52 deletions(-) diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index 9f98719..bbf35da 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -210,63 +210,89 @@ namespace { "};\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" "vertex VertexOut vs_color(VertexColorIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\n" " VertexOut out;\n" - " out.position = c.mvp * float4(in.position, 1.0);\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)]]) {\n" " VertexOut out;\n" - " out.position = c.mvp * float4(in.position, 1.0);\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 = in.texcoord;\n" " out.texcoord1 = in.texcoord;\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)]]) {\n" " VertexOut out;\n" - " out.position = c.mvp * float4(in.position, 1.0);\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 = in.texcoord;\n" " out.texcoord1 = in.texcoord1;\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)]]) {\n" " VertexOut out;\n" - " out.position = c.mvp * float4(in.position, 1.0);\n" + " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" + " out.position = pos4;\n" " out.color = in.color;\n" " out.texcoord = in.texcoord;\n" " out.texcoord1 = in.texcoord;\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)]]) {\n" " VertexOut out;\n" - " out.position = c.mvp * float4(in.position, 1.0);\n" + " float4 pos4 = c.mvp * float4(in.position, 1.0);\n" + " out.position = pos4;\n" " out.color = in.color;\n" " out.texcoord = in.texcoord;\n" " out.texcoord1 = in.texcoord1;\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" - " out.position = c.mvp * float4(in.position, 1.0);\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" @@ -286,10 +312,13 @@ namespace { " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" - " out.position = c.mvp * float4(skinned, 1.0);\n" + " float4 pos4 = c.mvp * float4(skinned, 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_tex(VertexSkinTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]], constant float4* vc [[buffer(2)]]) {\n" @@ -309,10 +338,13 @@ namespace { " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" - " out.position = c.mvp * float4(skinned, 1.0);\n" + " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" + " out.position = pos4;\n" " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" " out.texcoord = in.texcoord;\n" " out.texcoord1 = in.texcoord;\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" @@ -332,25 +364,42 @@ namespace { " p.z = dot(pos, c2);\n" " skinned += p * w[i];\n" " }\n" - " out.position = c.mvp * float4(skinned, 1.0);\n" + " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" + " out.position = pos4;\n" " out.color = float4(1.0, 1.0, 1.0, 1.0);\n" " out.texcoord = in.texcoord;\n" " out.texcoord1 = in.texcoord1;\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 color;\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 color;\n" + " return applyFog(color, in.viewZ, ps);\n" "}\n"; } @@ -383,7 +432,98 @@ void CGxDeviceMTL::ITexMarkAsUpdated(CGxTex* texId) { } void CGxDeviceMTL::IRsSendToHw(EGxRenderState which) { - (void)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) { @@ -647,7 +787,17 @@ void* CGxDeviceMTL::GetPipeline(EGxVertexBufferFormat format, bool useColor, boo } } - psName = useTexPipeline ? @"ps_tex" : @"ps_main"; + // 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]; @@ -739,6 +889,10 @@ void* CGxDeviceMTL::GetPipeline(EGxVertexBufferFormat format, bool useColor, boo 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]; @@ -919,9 +1073,20 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { 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) { @@ -931,6 +1096,21 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { } } } + // 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) { @@ -947,11 +1127,16 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { return; } - C44Matrix mvp; + // 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; - // TODO: Restore shader constant usage once verified. - // For now, we debug log if a vertex shader is present to analyze the constants. + // 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) { @@ -963,16 +1148,29 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { 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) { - mvp = this->m_projNative; + vsConsts.mvp = this->m_projNative; } else { const auto& world = this->m_xforms[GxXform_World].TopConst(); const auto& view = this->m_xforms[GxXform_View].TopConst(); - mvp = (world * view) * this->m_projNative; + 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; } } @@ -992,17 +1190,39 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { 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]; - [encoder setFrontFacingWinding:(cullMode == 1) ? MTLWindingClockwise : MTLWindingCounterClockwise]; + // 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:&mvp length:sizeof(mvp) atIndex:1]; + [encoder setVertexBytes:&vsConsts length:sizeof(vsConsts) atIndex:1]; if (useSkin) { [encoder setVertexBytes:CGxDevice::s_shadowConstants[1].constants length:sizeof(CGxDevice::s_shadowConstants[1].constants) @@ -1019,6 +1239,14 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { 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]; @@ -1038,14 +1266,29 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { struct MtlPSConstants { float alphaRef; - float pad[3]; + 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; - psConsts.pad[0] = psConsts.pad[1] = psConsts.pad[2] = 0.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 to white + // 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. @@ -1064,9 +1307,10 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { if (lighting == 0) { const auto& c = CGxDevice::s_shadowConstants[0].constants[0]; - // Sanity check: Only apply if values are within a reasonable range for color/alpha. - // This filters out uninitialized constants (often FLT_MAX) or non-color data. - if (std::abs(c.x) <= 10.0f && std::abs(c.y) <= 10.0f && std::abs(c.z) <= 10.0f && std::abs(c.w) <= 10.0f) { + // 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; @@ -1075,6 +1319,15 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { } } + // 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); @@ -1321,7 +1574,16 @@ void CGxDeviceMTL::ITexCreate(CGxTex* texId) { this->ITexWHDStartEnd(texId, width, height, baseMip, mipCount); auto device = (id)this->m_device; - auto desc = [MTLTextureDescriptor texture2DDescriptorWithPixelFormat:pixelFormat width:width height:height mipmapped:(mipCount - baseMip) > 1]; + 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; @@ -1358,35 +1620,48 @@ void CGxDeviceMTL::ITexUpload(CGxTex* texId) { 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 (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), - 0, - mipLevel, - texId->m_userArg, - texelStrideInBytes, - texels - ); + 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; - } + 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); + 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; - [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:bytesPerRow]; - } else { - [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:texelStrideInBytes]; + 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 { + if (texId->m_target == GxTex_CubeMap) { + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip slice:face withBytes:texels bytesPerRow:texelStrideInBytes bytesPerImage:0]; + } else { + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:texelStrideInBytes]; + } + } } } From 7fdd22545f81a1d146eb823898baae0b248cdba1 Mon Sep 17 00:00:00 2001 From: aomizu Date: Thu, 25 Dec 2025 13:04:03 +0900 Subject: [PATCH 5/8] feat: Convert GxTex_Argb4444 textures to RGBA8 during Metal upload to simplify handling --- src/gx/mtl/CGxDeviceMTL.mm | 45 +++++++++++++++++++++++++++++++++++--- 1 file changed, 42 insertions(+), 3 deletions(-) diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index bbf35da..617ead1 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -12,6 +12,7 @@ #include #include #include +#include #import #import @@ -42,13 +43,19 @@ namespace { } 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: - return MTLPixelFormatABGR4Unorm; + // Convert ARGB4444 to RGBA8 during upload for simpler handling + return MTLPixelFormatRGBA8Unorm; case GxTex_Argb1555: return MTLPixelFormatBGR5A1Unorm; case GxTex_Rgb565: @@ -1656,10 +1663,42 @@ void CGxDeviceMTL::ITexUpload(CGxTex* texId) { [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:texels bytesPerRow:texelStrideInBytes bytesPerImage:0]; + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip slice:face withBytes:uploadTexels bytesPerRow:texelStrideInBytes bytesPerImage:0]; } else { - [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:texels bytesPerRow:texelStrideInBytes]; + [texture replaceRegion:region mipmapLevel:mipLevel - baseMip withBytes:uploadTexels bytesPerRow:texelStrideInBytes]; } } } From 8fb51991e0eaae9cdc4f3cc61a435573764ae4f6 Mon Sep 17 00:00:00 2001 From: aomizu Date: Thu, 25 Dec 2025 13:24:07 +0900 Subject: [PATCH 6/8] revert: remove non-metal shader init from Client.cpp --- src/client/Client.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/client/Client.cpp b/src/client/Client.cpp index f563387..8863218 100644 --- a/src/client/Client.cpp +++ b/src/client/Client.cpp @@ -11,7 +11,6 @@ #include "glue/CGlueMgr.hpp" #include "glue/GlueScriptEvents.hpp" #include "gx/Screen.hpp" -#include "gx/Shader.hpp" #include "gx/Texture.hpp" #include "model/Model2.hpp" #include "net/Poll.hpp" @@ -236,7 +235,6 @@ int32_t InitializeEngineCallback(const void* a1, void* a2) { auto m2Flags = M2RegisterCVars(); M2Initialize(m2Flags, 0); - CShaderEffect::InitShaderSystem((M2GetCacheFlags() & 0x8) != 0, 0); // v4 = *(_DWORD *)(dword_B2FA00 + 48); // sub_4B61C0(dword_AB6128[v4]); From 7cf71278109232780454bb723dabf776d942d1fb Mon Sep 17 00:00:00 2001 From: aomizu Date: Thu, 25 Dec 2025 15:10:54 +0900 Subject: [PATCH 7/8] feat: Implement color animation in Metal shaders by using diffuse and emissive vertex constants for output color. --- src/gx/mtl/CGxDeviceMTL.mm | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index 617ead1..9d9273c 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -321,7 +321,11 @@ namespace { " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" - " out.color = float4(1.0, 1.0, 1.0, 1.0);\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" @@ -347,7 +351,11 @@ namespace { " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" - " out.color = float4(1.0, 1.0, 1.0, 1.0);\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 = in.texcoord;\n" " out.texcoord1 = in.texcoord;\n" " out.viewZ = pos4.w;\n" @@ -373,7 +381,11 @@ namespace { " }\n" " float4 pos4 = c.mvp * float4(skinned, 1.0);\n" " out.position = pos4;\n" - " out.color = float4(1.0, 1.0, 1.0, 1.0);\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 = in.texcoord;\n" " out.texcoord1 = in.texcoord1;\n" " out.viewZ = pos4.w;\n" From 8935c520c068ee36caeddac3613c5999e2aa6e92 Mon Sep 17 00:00:00 2001 From: aomizu Date: Fri, 26 Dec 2025 17:03:37 +0900 Subject: [PATCH 8/8] feat: Add texture matrix transform support to Metal shaders for animated textures --- src/gx/mtl/CGxDeviceMTL.mm | 48 ++++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/src/gx/mtl/CGxDeviceMTL.mm b/src/gx/mtl/CGxDeviceMTL.mm index 9d9273c..f2f8c5d 100644 --- a/src/gx/mtl/CGxDeviceMTL.mm +++ b/src/gx/mtl/CGxDeviceMTL.mm @@ -236,6 +236,18 @@ namespace { " 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" @@ -247,46 +259,46 @@ namespace { " out.pointSize = c.pointSize;\n" " return out;\n" "}\n" - "vertex VertexOut vs_tex(VertexTexIn in [[stage_in]], constant VSConstants& c [[buffer(1)]]) {\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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord;\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)]]) {\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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord1;\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)]]) {\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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord;\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)]]) {\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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord1;\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" @@ -356,8 +368,8 @@ namespace { " 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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord;\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" @@ -386,8 +398,8 @@ namespace { " 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 = in.texcoord;\n" - " out.texcoord1 = in.texcoord1;\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" @@ -1242,7 +1254,9 @@ void CGxDeviceMTL::Draw(CGxBatch* batch, int32_t indexed) { [encoder setVertexBuffer:mtlVertexBuf offset:vertexBuf->m_index atIndex:0]; [encoder setVertexBytes:&vsConsts length:sizeof(vsConsts) atIndex:1]; - if (useSkin) { + // 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];