diff --git a/Client/core/Graphics/CGraphics.cpp b/Client/core/Graphics/CGraphics.cpp index c60437761ad..3706f894aaa 100644 --- a/Client/core/Graphics/CGraphics.cpp +++ b/Client/core/Graphics/CGraphics.cpp @@ -2786,27 +2786,42 @@ void CGraphics::DrawProgressMessage(bool bPreserveBackbuffer) // ///////////////////////////////////////////////////////////// bool CGraphics::ResizeTextureData(const void* pData, uint uiDataPitch, uint uiWidth, uint uiHeight, uint d3dFormat, uint uiNewWidth, uint uiNewHeight, - CBuffer& outBuffer) + CBuffer& outBuffer, uint d3dNewFormat) { bool bResult = false; IDirect3DSurface9* pCurrentSurface = NULL; IDirect3DSurface9* pNewSurface = NULL; + // Output format defaults to the source format. + const D3DFORMAT destFormat = d3dNewFormat ? (D3DFORMAT)d3dNewFormat : (D3DFORMAT)d3dFormat; + const bool bConvertFormat = destFormat != (D3DFORMAT)d3dFormat; + do { - // Create surfaces - if (FAILED(g_pGraphics->GetDevice()->CreateOffscreenPlainSurface(uiWidth, uiHeight, (D3DFORMAT)d3dFormat, D3DPOOL_SCRATCH, &pCurrentSurface, NULL))) - break; - if (FAILED(g_pGraphics->GetDevice()->CreateOffscreenPlainSurface(uiNewWidth, uiNewHeight, (D3DFORMAT)d3dFormat, D3DPOOL_SCRATCH, &pNewSurface, NULL))) - break; - - // Data in - if (!CopyDataToSurface(pCurrentSurface, (const BYTE*)pData, uiDataPitch)) + // Destination surface (resized and/or reformatted) + if (FAILED(g_pGraphics->GetDevice()->CreateOffscreenPlainSurface(uiNewWidth, uiNewHeight, destFormat, D3DPOOL_SCRATCH, &pNewSurface, NULL))) break; - // Resize - if (FAILED(D3DXLoadSurfaceFromSurface(pNewSurface, NULL, NULL, pCurrentSurface, NULL, NULL, D3DX_FILTER_TRIANGLE, 0))) - break; + if (bConvertFormat) + { + // Format conversion (e.g. a DXT-compressed source to A8R8G8B8). Load straight from + // memory: D3DXLoadSurfaceFromMemory decodes the source format (decompressing DXT) and + // resizes into the destination surface in one step. This is more reliable than copying + // compressed blocks into an intermediate surface by hand. + RECT srcRect = {0, 0, (LONG)uiWidth, (LONG)uiHeight}; + if (FAILED(D3DXLoadSurfaceFromMemory(pNewSurface, NULL, NULL, pData, (D3DFORMAT)d3dFormat, uiDataPitch, NULL, &srcRect, D3DX_FILTER_TRIANGLE, 0))) + break; + } + else + { + // Same-format resize: copy the source pixels into a matching surface and resize + if (FAILED(g_pGraphics->GetDevice()->CreateOffscreenPlainSurface(uiWidth, uiHeight, (D3DFORMAT)d3dFormat, D3DPOOL_SCRATCH, &pCurrentSurface, NULL))) + break; + if (!CopyDataToSurface(pCurrentSurface, (const BYTE*)pData, uiDataPitch)) + break; + if (FAILED(D3DXLoadSurfaceFromSurface(pNewSurface, NULL, NULL, pCurrentSurface, NULL, NULL, D3DX_FILTER_TRIANGLE, 0))) + break; + } // Data out if (!CopyDataFromSurface(pNewSurface, outBuffer)) diff --git a/Client/core/Graphics/CGraphics.h b/Client/core/Graphics/CGraphics.h index 84186c13703..37f4d8e6a5e 100644 --- a/Client/core/Graphics/CGraphics.h +++ b/Client/core/Graphics/CGraphics.h @@ -186,7 +186,7 @@ class CGraphics : public CGraphicsInterface, public CSingleton // Texture data manipulation bool ResizeTextureData(const void* pData, uint uiDataPitch, uint uiWidth, uint uiHeight, uint d3dFormat, uint uiNewWidth, uint uiNewHeight, - CBuffer& outBuffer); + CBuffer& outBuffer, uint d3dNewFormat = 0); bool CopyDataToSurface(IDirect3DSurface9* pSurface, const uchar* pPixelsData, uint uiDataPitch); bool CopyDataFromSurface(IDirect3DSurface9* pSurface, CBuffer& outBuffer); diff --git a/Client/game_sa/CRenderWareSA.ClothesReplacing.cpp b/Client/game_sa/CRenderWareSA.ClothesReplacing.cpp index fbf7d78548c..ad1db8f2809 100644 --- a/Client/game_sa/CRenderWareSA.ClothesReplacing.cpp +++ b/Client/game_sa/CRenderWareSA.ClothesReplacing.cpp @@ -345,4 +345,5 @@ static void __declspec(naked) HOOK_CStreaming_RequestModel_Mid() void CRenderWareSA::StaticSetClothesReplacingHooks() { EZHookInstall(CStreaming_RequestModel_Mid); + StaticSetClothesTexturesHooks(); } diff --git a/Client/game_sa/CRenderWareSA.ClothesTextures.cpp b/Client/game_sa/CRenderWareSA.ClothesTextures.cpp new file mode 100644 index 00000000000..060d425cd7d --- /dev/null +++ b/Client/game_sa/CRenderWareSA.ClothesTextures.cpp @@ -0,0 +1,539 @@ +/***************************************************************************** + * + * PROJECT: Multi Theft Auto v1.0 + * LICENSE: See LICENSE in the top level directory + * FILE: game_sa/CRenderWareSA.ClothesTextures.cpp + * PURPOSE: Resolution-agnostic reimplementation of the GTA SA clothes + * texture compositing primitives, allowing custom CJ clothing + * textures of any size (up to 1024x1024) instead of 256x256 + * + * Multi Theft Auto is available from https://www.multitheftauto.com/ + * RenderWare is © Criterion Software + * + *****************************************************************************/ + +#include "StdInc.h" +#include +#include +#include "CGameSA.h" +#include "CRenderWareSA.h" +#include "gamesa_renderware.h" +#include +#include + +extern CGameSA* pGame; + +namespace +{ + constexpr int MAX_DIMENSIONS = 1024; + + // RenderWare raster constants + constexpr int RW_RASTER_TYPE_TEXTURE = 0x04; + constexpr int RW_RASTER_LOCK_READ = 0x01; + constexpr int RW_RASTER_LOCK_WRITE = 0x02; + constexpr int RW_RASTER_LOCK_READWRITE = 0x03; + constexpr unsigned int RW_FILTER_LINEAR = 0x02; + + // GTA SA clothes builder texture compositing primitives + constexpr DWORD ADDR_CopyTexture = 0x5A5730; + constexpr DWORD ADDR_PlaceTextureOnTopOfTexture = 0x5A57B0; + constexpr DWORD ADDR_BlendTextures_DstSrc = 0x5A5820; + constexpr DWORD ADDR_BlendTextures_DstSrc1Src2 = 0x5A59C0; + constexpr DWORD ADDR_BlendTextures_DstSrc1Src2Tat = 0x5A5BC0; + + inline std::uint8_t WeightedByte(std::uint8_t a, float ra, std::uint8_t b, float rb) + { + return static_cast(std::clamp(static_cast(a * ra + b * rb + 0.5f), 0, 255)); + } + + inline std::uint8_t WeightedByte(std::uint8_t a, float ra, std::uint8_t b, float rb, std::uint8_t c, float rc) + { + return static_cast(std::clamp(static_cast(a * ra + b * rb + c * rc + 0.5f), 0, 255)); + } + + inline std::uint8_t LerpByte(std::uint8_t a, std::uint8_t b, float t) + { + return static_cast(std::clamp(static_cast(a * (1.0f - t) + b * t + 0.5f), 0, 255)); + } + + // + // Bilinear resample of a 32bpp (4 bytes/pixel) image. Channels are treated + // independently so the byte order (BGRA/ARGB) is irrelevant. The clothes + // rasters are power-of-two so a tightly packed width*4 stride matches what + // the game itself assumes in its compositing loops. + // + void ResampleRGBA32(const std::uint32_t* pSrc, int srcWidth, int srcHeight, std::uint32_t* pDst, int dstWidth, int dstHeight) + { + if (srcWidth == dstWidth && srcHeight == dstHeight) + { + std::memcpy(pDst, pSrc, static_cast(dstWidth) * dstHeight * 4); + return; + } + + for (int y = 0; y < dstHeight; ++y) + { + const float fy = (dstHeight > 1) ? static_cast(y) * (srcHeight - 1) / (dstHeight - 1) : 0.0f; + const int y0 = static_cast(fy); + const int y1 = std::min(y0 + 1, srcHeight - 1); + const float wy = fy - y0; + + for (int x = 0; x < dstWidth; ++x) + { + const float fx = (dstWidth > 1) ? static_cast(x) * (srcWidth - 1) / (dstWidth - 1) : 0.0f; + const int x0 = static_cast(fx); + const int x1 = std::min(x0 + 1, srcWidth - 1); + const float wx = fx - x0; + + const std::uint32_t c00 = pSrc[y0 * srcWidth + x0]; + const std::uint32_t c10 = pSrc[y0 * srcWidth + x1]; + const std::uint32_t c01 = pSrc[y1 * srcWidth + x0]; + const std::uint32_t c11 = pSrc[y1 * srcWidth + x1]; + + std::uint32_t out = 0; + for (int shift = 0; shift < 32; shift += 8) + { + const float top = ((c00 >> shift) & 0xFF) * (1.0f - wx) + ((c10 >> shift) & 0xFF) * wx; + const float bot = ((c01 >> shift) & 0xFF) * (1.0f - wx) + ((c11 >> shift) & 0xFF) * wx; + int v = static_cast(top * (1.0f - wy) + bot * wy + 0.5f); + v = std::clamp(v, 0, 255); + out |= static_cast(v) << shift; + } + pDst[y * dstWidth + x] = out; + } + } + } + + // + // Create a new raster (dstWidth x dstHeight) whose pixels are a resampled + // copy of pSrcRaster. Mirrors the raster flags the game's own CopyTexture + // uses: (format << 8) | rwRASTERTYPETEXTURE. Returns nullptr on failure. + // + RwRaster* CreateResampledRaster(RwRaster* pSrcRaster, int dstWidth, int dstHeight) + { + if (!pSrcRaster) + return nullptr; + + const int flags = (static_cast(pSrcRaster->format) << 8) | RW_RASTER_TYPE_TEXTURE; + RwRaster* pNewRaster = RwRasterCreate(dstWidth, dstHeight, pSrcRaster->depth, flags); + if (!pNewRaster) + return nullptr; + + auto* pSrcBits = reinterpret_cast(RwRasterLock(pSrcRaster, 0, RW_RASTER_LOCK_READ)); + auto* pDstBits = reinterpret_cast(RwRasterLock(pNewRaster, 0, RW_RASTER_LOCK_WRITE)); + + if (pSrcBits && pDstBits) + ResampleRGBA32(pSrcBits, pSrcRaster->width, pSrcRaster->height, pDstBits, dstWidth, dstHeight); + + if (pDstBits) + RwRasterUnlock(pNewRaster); + if (pSrcBits) + RwRasterUnlock(pSrcRaster); + + return pNewRaster; + } + + // + // Grow/shrink an owned accumulator texture's raster in place to the given + // size. The old raster is destroyed. Returns false on failure (texture + // left untouched). + // + bool ResizeAccumulatorInPlace(RwTexture* pTexture, int width, int height) + { + RwRaster* pOldRaster = pTexture->raster; + if (!pOldRaster) + return false; + if (pOldRaster->width == width && pOldRaster->height == height) + return true; + + RwRaster* pNewRaster = CreateResampledRaster(pOldRaster, width, height); + if (!pNewRaster) + return false; + + pTexture->raster = pNewRaster; + RwRasterDestroy(pOldRaster); + return true; + } + + // + // Return a texture whose raster is exactly width x height. If pSrcTexture + // already matches, it is returned as-is (*pbIsTemporary = false). Otherwise + // a temporary resampled texture is created (*pbIsTemporary = true) which the + // caller must RwTextureDestroy. Shared/streamed source textures must never + // be mutated, hence the copy. + // + RwTexture* GetSourceTextureAtSize(RwTexture* pSrcTexture, int width, int height, bool* pbIsTemporary) + { + *pbIsTemporary = false; + RwRaster* pSrcRaster = pSrcTexture->raster; + if (!pSrcRaster) + return pSrcTexture; + + // A non-32bpp source (e.g. a DXT-compressed replacement) must be decompressed to 32bpp + // before the CPU pixel compositing can use it. CreateResizedTexture decompresses and + // resizes to A8R8G8B8 in one step. + if (pSrcRaster->depth != 32) + { + SString strError; + RwTexture* pConverted = pGame->GetRenderWareSA()->CreateResizedTexture(pSrcTexture, width, height, strError, D3DFMT_A8R8G8B8, true); + if (!pConverted) + return pSrcTexture; + *pbIsTemporary = true; + return pConverted; + } + + // Already 32bpp and the right size: use as-is + if (pSrcRaster->width == width && pSrcRaster->height == height) + return pSrcTexture; + + // 32bpp but a different size: CPU resample + RwRaster* pNewRaster = CreateResampledRaster(pSrcRaster, width, height); + if (!pNewRaster) + return pSrcTexture; + + RwTexture* pTempTexture = RwTextureCreate(pNewRaster); + if (!pTempTexture) + { + RwRasterDestroy(pNewRaster); + return pSrcTexture; + } + + *pbIsTemporary = true; + return pTempTexture; + } + + inline int CapDimension(int value) + { + return std::min(value, MAX_DIMENSIONS); + } + + // The clothes compositing is CPU pixel math, so it only works on uncompressed 32bpp rasters. + // Non-32bpp sources are decompressed/expanded to 32bpp on the fly; this is a last-resort guard + // so a raster that still isn't 32bpp (a conversion failure) is never read as 32bpp here, which + // would read past the (smaller) buffer and crash. + inline bool IsRaster32bpp(RwRaster* pRaster) + { + return pRaster && pRaster->depth == 32; + } +} // namespace + +// +// Replacement for CClothesBuilder::CopyTexture (0x5A5730). +// Clones a texture as a fresh compositing base. Caps the clone at MAX_DIMENSIONS and decompresses +// non-32bpp (e.g. DXT) sources to 32bpp. Also folds in the NULL-check that multiplayer_sa used to +// provide as a separate hook at this address. +// +static RwTexture* __cdecl HOOK_CClothesBuilder_CopyTexture(RwTexture* pSrcTexture) +{ + if (!pSrcTexture || !pSrcTexture->raster) + { + AddReportLog(9420, "ClothesTextures: CopyTexture called with NULL texture/raster"); + return nullptr; + } + + RwRaster* pSrcRaster = pSrcTexture->raster; + const int width = CapDimension(pSrcRaster->width); + const int height = CapDimension(pSrcRaster->height); + + // Non-32bpp (e.g. DXT) source: decompress to a 32bpp A8R8G8B8 base + if (pSrcRaster->depth != 32) + { + SString strError; + RwTexture* pConverted = pGame->GetRenderWareSA()->CreateResizedTexture(pSrcTexture, width, height, strError, D3DFMT_A8R8G8B8, true); + if (!pConverted) + { + AddReportLog(9421, SString("ClothesTextures: CopyTexture could not decompress a non-32bpp raster (depth %d): %s", pSrcRaster->depth, *strError)); + return nullptr; + } + pConverted->flags = (pConverted->flags & 0xFFFFFF00) | RW_FILTER_LINEAR; + return pConverted; + } + + RwRaster* pNewRaster = CreateResampledRaster(pSrcRaster, width, height); + if (!pNewRaster) + return nullptr; + + RwTexture* pNewTexture = RwTextureCreate(pNewRaster); + if (!pNewTexture) + { + RwRasterDestroy(pNewRaster); + return nullptr; + } + + // Match the original: set filter mode to linear (byte at RwTexture+0x50) + pNewTexture->flags = (pNewTexture->flags & 0xFFFFFF00) | RW_FILTER_LINEAR; + return pNewTexture; +} + +// +// Replacement for CClothesBuilder::PlaceTextureOnTopOfTexture (0x5A57B0). +// Overlays the (alpha != 0) pixels of srcTex onto dstTex. Equalizes both +// textures to a common size first (growing the owned dst accumulator and +// resampling the shared src into a temporary), eliminating the original's +// fixed-size assumption that caused the heap overflow. +// +static void __cdecl HOOK_CClothesBuilder_PlaceTextureOnTopOfTexture(RwTexture* pDstTexture, RwTexture* pSrcTexture) +{ + if (!pDstTexture || !pSrcTexture || !pDstTexture->raster || !pSrcTexture->raster) + return; + + const int width = CapDimension(std::max(pDstTexture->raster->width, pSrcTexture->raster->width)); + const int height = CapDimension(std::max(pDstTexture->raster->height, pSrcTexture->raster->height)); + + ResizeAccumulatorInPlace(pDstTexture, width, height); + + bool bSrcTemporary = false; + RwTexture* pSrc = GetSourceTextureAtSize(pSrcTexture, width, height, &bSrcTemporary); + + RwRaster* pDstRaster = pDstTexture->raster; + RwRaster* pSrcRaster = pSrc->raster; + + // Safety net: both must be 32bpp now (sources are decompressed by GetSourceTextureAtSize). Skip + // if a conversion failed rather than read a smaller buffer as 32bpp. + if (!IsRaster32bpp(pDstRaster) || !IsRaster32bpp(pSrcRaster)) + { + if (bSrcTemporary) + RwTextureDestroy(pSrc); + return; + } + + // Defensive: never iterate past the smaller of the two buffers + const int pixelCount = std::min(pDstRaster->width * pDstRaster->height, pSrcRaster->width * pSrcRaster->height); + + auto* pDstBits = reinterpret_cast(RwRasterLock(pDstRaster, 0, RW_RASTER_LOCK_READWRITE)); + auto* pSrcBits = reinterpret_cast(RwRasterLock(pSrcRaster, 0, RW_RASTER_LOCK_READWRITE)); + + if (pDstBits && pSrcBits) + { + for (int i = 0; i < pixelCount; ++i) + { + if (pSrcBits[i] & 0xFF000000) + pDstBits[i] = pSrcBits[i]; + } + } + + if (pSrcBits) + RwRasterUnlock(pSrcRaster); + if (pDstBits) + RwRasterUnlock(pDstRaster); + + if (bSrcTemporary) + RwTextureDestroy(pSrc); +} + +// +// Replacement for CClothesBuilder::BlendTextures (0x5A5820) - "Dst-Src". +// Per RGB channel: dst = dst*r1 + src*r2 (alpha preserved). Used e.g. for the +// face fatness blend. Equalizes sizes so a replaced face texture can be HD. +// +static void __cdecl HOOK_CClothesBuilder_BlendTextures_DstSrc(RwTexture* pDstTexture, RwTexture* pSrcTexture, float r1, float r2, int /*numColors*/) +{ + if (!pDstTexture || !pSrcTexture || !pDstTexture->raster || !pSrcTexture->raster) + return; + + const int width = CapDimension(std::max(pDstTexture->raster->width, pSrcTexture->raster->width)); + const int height = CapDimension(std::max(pDstTexture->raster->height, pSrcTexture->raster->height)); + + ResizeAccumulatorInPlace(pDstTexture, width, height); + bool bSrcTemp = false; + RwTexture* pSrc = GetSourceTextureAtSize(pSrcTexture, width, height, &bSrcTemp); + + RwRaster* pDstRaster = pDstTexture->raster; + RwRaster* pSrcRaster = pSrc->raster; + + if (!IsRaster32bpp(pDstRaster) || !IsRaster32bpp(pSrcRaster)) + { + if (bSrcTemp) + RwTextureDestroy(pSrc); + return; + } + + const int pixelCount = std::min(pDstRaster->width * pDstRaster->height, pSrcRaster->width * pSrcRaster->height); + + auto* pDst = reinterpret_cast(RwRasterLock(pDstRaster, 0, RW_RASTER_LOCK_READWRITE)); + auto* pSrcBits = reinterpret_cast(RwRasterLock(pSrcRaster, 0, RW_RASTER_LOCK_READ)); + + if (pDst && pSrcBits) + { + for (int i = 0; i < pixelCount; ++i) + { + std::uint8_t* d = pDst + i * 4; + std::uint8_t* s = pSrcBits + i * 4; + d[0] = WeightedByte(d[0], r1, s[0], r2); + d[1] = WeightedByte(d[1], r1, s[1], r2); + d[2] = WeightedByte(d[2], r1, s[2], r2); + // alpha (d[3]) unchanged + } + } + + if (pSrcBits) + RwRasterUnlock(pSrcRaster); + if (pDst) + RwRasterUnlock(pDstRaster); + if (bSrcTemp) + RwTextureDestroy(pSrc); +} + +// +// Replacement for CClothesBuilder::BlendTextures (0x5A59C0) - "Dst-Src1-Src2". +// Per RGB channel: dst = dst*r1 + src1*r2 + src2*r3 (alpha preserved). This is +// the body shape (normal/fat/muscle) blend, used when no clothing overlay is +// present for the part. +// +static void __cdecl HOOK_CClothesBuilder_BlendTextures_DstSrc1Src2(RwTexture* pDstTexture, RwTexture* pSrc1Texture, RwTexture* pSrc2Texture, float r1, float r2, + float r3, int /*numColors*/) +{ + if (!pDstTexture || !pSrc1Texture || !pSrc2Texture || !pDstTexture->raster || !pSrc1Texture->raster || !pSrc2Texture->raster) + return; + + int width = std::max({pDstTexture->raster->width, pSrc1Texture->raster->width, pSrc2Texture->raster->width}); + int height = std::max({pDstTexture->raster->height, pSrc1Texture->raster->height, pSrc2Texture->raster->height}); + width = CapDimension(width); + height = CapDimension(height); + + ResizeAccumulatorInPlace(pDstTexture, width, height); + bool bSrc1Temp = false, bSrc2Temp = false; + RwTexture* pSrc1 = GetSourceTextureAtSize(pSrc1Texture, width, height, &bSrc1Temp); + RwTexture* pSrc2 = GetSourceTextureAtSize(pSrc2Texture, width, height, &bSrc2Temp); + + RwRaster* pDstRaster = pDstTexture->raster; + RwRaster* pSrc1Raster = pSrc1->raster; + RwRaster* pSrc2Raster = pSrc2->raster; + + if (!IsRaster32bpp(pDstRaster) || !IsRaster32bpp(pSrc1Raster) || !IsRaster32bpp(pSrc2Raster)) + { + if (bSrc2Temp) + RwTextureDestroy(pSrc2); + if (bSrc1Temp) + RwTextureDestroy(pSrc1); + return; + } + + const int pixelCount = + std::min({pDstRaster->width * pDstRaster->height, pSrc1Raster->width * pSrc1Raster->height, pSrc2Raster->width * pSrc2Raster->height}); + + auto* pDst = reinterpret_cast(RwRasterLock(pDstRaster, 0, RW_RASTER_LOCK_READWRITE)); + auto* pS1 = reinterpret_cast(RwRasterLock(pSrc1Raster, 0, RW_RASTER_LOCK_READ)); + auto* pS2 = reinterpret_cast(RwRasterLock(pSrc2Raster, 0, RW_RASTER_LOCK_READ)); + + if (pDst && pS1 && pS2) + { + for (int i = 0; i < pixelCount; ++i) + { + std::uint8_t* d = pDst + i * 4; + std::uint8_t* a = pS1 + i * 4; + std::uint8_t* b = pS2 + i * 4; + d[0] = WeightedByte(d[0], r1, a[0], r2, b[0], r3); + d[1] = WeightedByte(d[1], r1, a[1], r2, b[1], r3); + d[2] = WeightedByte(d[2], r1, a[2], r2, b[2], r3); + // alpha (d[3]) unchanged + } + } + + if (pS2) + RwRasterUnlock(pSrc2Raster); + if (pS1) + RwRasterUnlock(pSrc1Raster); + if (pDst) + RwRasterUnlock(pDstRaster); + if (bSrc2Temp) + RwTextureDestroy(pSrc2); + if (bSrc1Temp) + RwTextureDestroy(pSrc1); +} + +// +// Replacement for CClothesBuilder::BlendTextures (0x5A5BC0) - "Dst-Src1-Src2-Tat". +// Body shape blend (dst*r1 + src1*r2 + src2*r3) then alpha-composite the +// "tattoo" layer on top using the tattoo's own per-pixel alpha. For clothing +// this "tattoo" layer is the accumulated clothing/design overlay, so this is the +// key path for an HD shirt to reach the final torso texture. +// +static void __cdecl HOOK_CClothesBuilder_BlendTextures_DstSrc1Src2Tat(RwTexture* pDstTexture, RwTexture* pSrc1Texture, RwTexture* pSrc2Texture, float r1, + float r2, float r3, int /*numColors*/, RwTexture* pTatTexture) +{ + if (!pDstTexture || !pSrc1Texture || !pSrc2Texture || !pTatTexture || !pDstTexture->raster || !pSrc1Texture->raster || !pSrc2Texture->raster || + !pTatTexture->raster) + return; + + int width = std::max({pDstTexture->raster->width, pSrc1Texture->raster->width, pSrc2Texture->raster->width, pTatTexture->raster->width}); + int height = std::max({pDstTexture->raster->height, pSrc1Texture->raster->height, pSrc2Texture->raster->height, pTatTexture->raster->height}); + width = CapDimension(width); + height = CapDimension(height); + + ResizeAccumulatorInPlace(pDstTexture, width, height); + bool bSrc1Temp = false, bSrc2Temp = false, bTatTemp = false; + RwTexture* pSrc1 = GetSourceTextureAtSize(pSrc1Texture, width, height, &bSrc1Temp); + RwTexture* pSrc2 = GetSourceTextureAtSize(pSrc2Texture, width, height, &bSrc2Temp); + RwTexture* pTat = GetSourceTextureAtSize(pTatTexture, width, height, &bTatTemp); + + RwRaster* pDstRaster = pDstTexture->raster; + RwRaster* pSrc1Raster = pSrc1->raster; + RwRaster* pSrc2Raster = pSrc2->raster; + RwRaster* pTatRaster = pTat->raster; + + if (!IsRaster32bpp(pDstRaster) || !IsRaster32bpp(pSrc1Raster) || !IsRaster32bpp(pSrc2Raster) || !IsRaster32bpp(pTatRaster)) + { + if (bTatTemp) + RwTextureDestroy(pTat); + if (bSrc2Temp) + RwTextureDestroy(pSrc2); + if (bSrc1Temp) + RwTextureDestroy(pSrc1); + return; + } + + const int pixelCount = std::min({pDstRaster->width * pDstRaster->height, pSrc1Raster->width * pSrc1Raster->height, pSrc2Raster->width * pSrc2Raster->height, + pTatRaster->width * pTatRaster->height}); + + auto* pDst = reinterpret_cast(RwRasterLock(pDstRaster, 0, RW_RASTER_LOCK_READWRITE)); + auto* pS1 = reinterpret_cast(RwRasterLock(pSrc1Raster, 0, RW_RASTER_LOCK_READ)); + auto* pS2 = reinterpret_cast(RwRasterLock(pSrc2Raster, 0, RW_RASTER_LOCK_READ)); + auto* pT = reinterpret_cast(RwRasterLock(pTatRaster, 0, RW_RASTER_LOCK_READ)); + + if (pDst && pS1 && pS2 && pT) + { + for (int i = 0; i < pixelCount; ++i) + { + std::uint8_t* d = pDst + i * 4; + std::uint8_t* a = pS1 + i * 4; + std::uint8_t* b = pS2 + i * 4; + std::uint8_t* t = pT + i * 4; + const float tatAlpha = t[3] / 255.0f; + for (int c = 0; c < 3; ++c) + { + const std::uint8_t body = WeightedByte(d[c], r1, a[c], r2, b[c], r3); + d[c] = LerpByte(body, t[c], tatAlpha); + } + // alpha (d[3]) unchanged + } + } + + if (pT) + RwRasterUnlock(pTatRaster); + if (pS2) + RwRasterUnlock(pSrc2Raster); + if (pS1) + RwRasterUnlock(pSrc1Raster); + if (pDst) + RwRasterUnlock(pDstRaster); + if (bTatTemp) + RwTextureDestroy(pTat); + if (bSrc2Temp) + RwTextureDestroy(pSrc2); + if (bSrc1Temp) + RwTextureDestroy(pSrc1); +} + +////////////////////////////////////////////////////////////////////////////////////////// +// +// Install the clothes texture compositing hooks +// +////////////////////////////////////////////////////////////////////////////////////////// +void CRenderWareSA::StaticSetClothesTexturesHooks() +{ + HookInstall(ADDR_CopyTexture, (DWORD)HOOK_CClothesBuilder_CopyTexture, 5); + HookInstall(ADDR_PlaceTextureOnTopOfTexture, (DWORD)HOOK_CClothesBuilder_PlaceTextureOnTopOfTexture, 5); + HookInstall(ADDR_BlendTextures_DstSrc, (DWORD)HOOK_CClothesBuilder_BlendTextures_DstSrc, 5); + HookInstall(ADDR_BlendTextures_DstSrc1Src2, (DWORD)HOOK_CClothesBuilder_BlendTextures_DstSrc1Src2, 5); + HookInstall(ADDR_BlendTextures_DstSrc1Src2Tat, (DWORD)HOOK_CClothesBuilder_BlendTextures_DstSrc1Src2Tat, 5); +} diff --git a/Client/game_sa/CRenderWareSA.TxdRightSizing.cpp b/Client/game_sa/CRenderWareSA.TxdRightSizing.cpp index 95dfd43a0d9..c0c4b75d93e 100644 --- a/Client/game_sa/CRenderWareSA.TxdRightSizing.cpp +++ b/Client/game_sa/CRenderWareSA.TxdRightSizing.cpp @@ -125,29 +125,12 @@ RwTexture* CRenderWareSA::RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit } RwD3D9Raster* pD3DRaster = (RwD3D9Raster*)(&pRaster->renderResource); - if (!pD3DRaster->texture) - { - strError = "pD3DRaster->texture == NULL"; - return NULL; - } - if (!pD3DRaster->lockedSurface) - { - strError = "pD3DRaster->lockedSurface == NULL"; - return NULL; - } - if (!pD3DRaster->lockedRect.pBits) - { - strError = "pD3DRaster->lockedRect.pBits == NULL"; - return NULL; - } // Get texture info uint uiWidth = pRaster->width; uint uiHeight = pRaster->height; D3DFORMAT d3dFormat = pD3DRaster->format; - bool bHasAlpha = pD3DRaster->alpha != 0; bool bIsCubeTexture = (pD3DRaster->cubeTextureFlags & 0x01) != 0; - bool bHasMipMaps = (pRaster->numLevels > 1); bool bIsCompressed = (pD3DRaster->textureFlags & 0x10) != 0; // Check we can do this @@ -168,9 +151,48 @@ RwTexture* CRenderWareSA::RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit if (uiReqWidth == uiWidth && uiReqHeight == uiHeight) return NULL; - // Lock mip level 0 if required + return CreateResizedTexture(pTexture, uiReqWidth, uiReqHeight, strError); +} + +///////////////////////////////////////////////////////////////////////////// +// +// CRenderWareSA::CreateResizedTexture +// +// Create a copy of the supplied texture, resized to the given dimensions. +// Returns NULL on failure (strError is set). +// +///////////////////////////////////////////////////////////////////////////// +RwTexture* CRenderWareSA::CreateResizedTexture(RwTexture* pTexture, uint uiNewWidth, uint uiNewHeight, SString& strError, std::optional uiTargetD3DFormat, + std::optional bTargetAlpha) +{ + // Validate + RwRaster* pRaster = pTexture->raster; + if (!pRaster) + { + strError = "pRaster == NULL"; + return NULL; + } + + RwD3D9Raster* pD3DRaster = (RwD3D9Raster*)(&pRaster->renderResource); + if (!pD3DRaster->texture) + { + strError = "pD3DRaster->texture == NULL"; + return NULL; + } + + // Get texture info + uint uiWidth = pRaster->width; + uint uiHeight = pRaster->height; + D3DFORMAT d3dFormat = pD3DRaster->format; + bool bHasAlpha = pD3DRaster->alpha != 0; + bool bIsCubeTexture = (pD3DRaster->cubeTextureFlags & 0x01) != 0; + bool bIsCompressed = (pD3DRaster->textureFlags & 0x10) != 0; + + // Lock mip level 0. Use the cached lock if the raster already has one for level 0, otherwise + // lock the texture ourselves. Compressed textures (and others) are often not pre-locked, so we + // must not assume a cached lockedRect/lockedSurface is present. D3DLOCKED_RECT lockedRect = pD3DRaster->lockedRect; - bool bNeedOwnLock = (pD3DRaster->lockedLevel != 0) || !pD3DRaster->lockedSurface; + bool bNeedOwnLock = (pD3DRaster->lockedLevel != 0) || !pD3DRaster->lockedSurface || !pD3DRaster->lockedRect.pBits; if (bNeedOwnLock) if (FAILED(pD3DRaster->texture->LockRect(0, &lockedRect, NULL, D3DLOCK_NO_DIRTY_UPDATE | D3DLOCK_NOSYSLOCK | D3DLOCK_READONLY))) { @@ -178,10 +200,17 @@ RwTexture* CRenderWareSA::RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit return NULL; } + // When a caller asks for a specific target format (the clothes path) and the source is + // compressed (DXT) or not 32bpp, decompress/expand it to A8R8G8B8 - the clothes compositing + // only works on uncompressed 32bpp rasters. The right-sizing path passes no target format, + // so it keeps the original (e.g. DXT) format as before. + const bool bConvertToARGB = uiTargetD3DFormat.has_value() && (bIsCompressed || pRaster->depth != 32); + const uint uiResizeTargetFormat = bConvertToARGB ? static_cast(D3DFMT_A8R8G8B8) : 0; + // Try resize CBuffer newPixelBuffer; - bool bDidResize = - g_pCore->GetGraphics()->ResizeTextureData(lockedRect.pBits, lockedRect.Pitch, uiWidth, uiHeight, d3dFormat, uiReqWidth, uiReqHeight, newPixelBuffer); + bool bDidResize = g_pCore->GetGraphics()->ResizeTextureData(lockedRect.pBits, lockedRect.Pitch, uiWidth, uiHeight, d3dFormat, uiNewWidth, uiNewHeight, + newPixelBuffer, uiResizeTargetFormat); if (bNeedOwnLock) pD3DRaster->texture->UnlockRect(0); @@ -192,7 +221,47 @@ RwTexture* CRenderWareSA::RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit return NULL; } - // Make new RwTexture from pixels + // For the format-conversion case (e.g. DXT -> A8R8G8B8) build the texture directly from the + // decompressed 32bpp pixels. Going through rwD3D9NativeTextureRead with a hand-built header + // proved unreliable for converted formats, so create the raster and copy the pixels into it. + if (bConvertToARGB) + { + constexpr int RW_RASTERFORMAT_8888 = 0x0500; + constexpr int RW_RASTERTYPE_TEXTURE = 0x04; + + RwRaster* pNewRaster = RwRasterCreate(static_cast(uiNewWidth), static_cast(uiNewHeight), 32, RW_RASTERFORMAT_8888 | RW_RASTERTYPE_TEXTURE); + if (!pNewRaster) + { + strError = "RwRasterCreate failed"; + return NULL; + } + + unsigned char* pLocked = reinterpret_cast(RwRasterLock(pNewRaster, 0, 2 /* rwRASTERLOCKWRITE */)); + if (!pLocked) + { + RwRasterDestroy(pNewRaster); + strError = "RwRasterLock failed"; + return NULL; + } + memcpy(pLocked, newPixelBuffer.GetData(), std::min(newPixelBuffer.GetSize(), uiNewWidth * uiNewHeight * 4)); + RwRasterUnlock(pNewRaster); + + RwTexture* pConvertedTexture = RwTextureCreate(pNewRaster); + if (!pConvertedTexture) + { + RwRasterDestroy(pNewRaster); + strError = "RwTextureCreate failed"; + return NULL; + } + + // Preserve the original texture's name/mask (the clothes builder looks textures up by name) + memcpy(pConvertedTexture->name, pTexture->name, 32); + memcpy(pConvertedTexture->mask, pTexture->mask, 32); + pConvertedTexture->flags = pTexture->flags; + return pConvertedTexture; + } + + // Make new RwTexture from pixels (same-format resize path) NativeTexturePC_Header header; memset(&header, 0, sizeof(header)); header.TextureFormat.platformId = 9; @@ -202,16 +271,16 @@ RwTexture* CRenderWareSA::RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit memcpy(header.TextureFormat.name, pTexture->name, 32); memcpy(header.TextureFormat.maskName, pTexture->mask, 32); header.RasterFormat.rasterFormat = (pRaster->format & 0x0f) << 8; // ( dxt1 = 0x00000100 or 0x00000200 / dxt3 = 0x00000300 ) | 0x00008000 mipmaps? - header.RasterFormat.d3dFormat = pD3DRaster->format; - header.RasterFormat.width = static_cast(uiReqWidth); - header.RasterFormat.height = static_cast(uiReqHeight); + header.RasterFormat.d3dFormat = static_cast(uiTargetD3DFormat.value_or(static_cast(pD3DRaster->format))); header.RasterFormat.depth = static_cast(pRaster->depth); + header.RasterFormat.alpha = bTargetAlpha.value_or(bHasAlpha); + header.RasterFormat.compressed = bIsCompressed; + header.RasterFormat.width = static_cast(uiNewWidth); + header.RasterFormat.height = static_cast(uiNewHeight); header.RasterFormat.numLevels = 1; header.RasterFormat.rasterType = pRaster->type; // dxt1 = 4 / dxt3 = 4 - header.RasterFormat.alpha = bHasAlpha; header.RasterFormat.cubeTexture = bIsCubeTexture; header.RasterFormat.autoMipMaps = false; - header.RasterFormat.compressed = bIsCompressed; // Create stream containing new texture data CBuffer nativeData; diff --git a/Client/game_sa/CRenderWareSA.h b/Client/game_sa/CRenderWareSA.h index 1fc217ad967..027a4e8362d 100644 --- a/Client/game_sa/CRenderWareSA.h +++ b/Client/game_sa/CRenderWareSA.h @@ -120,6 +120,8 @@ class CRenderWareSA : public CRenderWare // CRenderWareSA methods RwTexture* RightSizeTexture(RwTexture* pTexture, uint uiSizeLimit, SString& strError); + RwTexture* CreateResizedTexture(RwTexture* pTexture, uint uiNewWidth, uint uiNewHeight, SString& strError, + std::optional uiTargetD3DFormat = std::nullopt, std::optional bTargetAlpha = std::nullopt); void ResetStats(); void GetShaderReplacementStats(SShaderReplacementStats& outStats); CModelTexturesInfo* GetModelTexturesInfo(ushort usModelId); @@ -128,6 +130,7 @@ class CRenderWareSA : public CRenderWare static void StaticSetHooks(); static void StaticSetClothesReplacingHooks(); + static void StaticSetClothesTexturesHooks(); static void RwTexDictionaryRemoveTexture(RwTexDictionary* pTXD, RwTexture* pTex); static bool RwTexDictionaryContainsTexture(RwTexDictionary* pTXD, RwTexture* pTex); static short CTxdStore_GetTxdRefcount(unsigned short usTxdID); diff --git a/Client/game_sa/gamesa_renderware.h b/Client/game_sa/gamesa_renderware.h index 224970c17cd..7eefaa21492 100644 --- a/Client/game_sa/gamesa_renderware.h +++ b/Client/game_sa/gamesa_renderware.h @@ -98,6 +98,7 @@ typedef int(__cdecl* rwD3D9NativeTextureRead_t)(RwStream* stream, RwTexture** te typedef RwRaster*(__cdecl* RwRasterUnlock_t)(RwRaster* raster); typedef RwRaster*(__cdecl* RwRasterLock_t)(RwRaster* raster, unsigned char level, int lockmode); typedef RwRaster*(__cdecl* RwRasterCreate_t)(int width, int height, int depth, int flags); +typedef RwRaster*(__cdecl* RwRasterDestroy_t)(RwRaster* raster); typedef RwTexture*(__cdecl* RwTextureCreate_t)(RwRaster* raster); typedef RpMaterial*(__cdecl* RpMaterialSetTexture_t)(RpMaterial* mat, RwTexture* tex); typedef RpHAnimHierarchy*(__cdecl* GetAnimHierarchyFromClump_t)(RpClump*); @@ -188,6 +189,7 @@ RWFUNC(RwTextureDestroy_t RwTextureDestroy, (RwTextureDestroy_t)0xDEAD) RWFUNC(RwRasterUnlock_t RwRasterUnlock, (RwRasterUnlock_t)0xDEAD) RWFUNC(RwRasterLock_t RwRasterLock, (RwRasterLock_t)0xDEAD) RWFUNC(RwRasterCreate_t RwRasterCreate, (RwRasterCreate_t)0xDEAD) +RWFUNC(RwRasterDestroy_t RwRasterDestroy, (RwRasterDestroy_t)0xDEAD) RWFUNC(RwTextureCreate_t RwTextureCreate, (RwTextureCreate_t)0xDEAD) RWFUNC(RpMaterialSetTexture_t RpMaterialSetTexture, (RpMaterialSetTexture_t)0xDEAD) RWFUNC(GetAnimHierarchyFromClump_t GetAnimHierarchyFromClump, (GetAnimHierarchyFromClump_t)0xDEAD) diff --git a/Client/game_sa/gamesa_renderware.hpp b/Client/game_sa/gamesa_renderware.hpp index 4f9bd21484c..14b04cf80cc 100644 --- a/Client/game_sa/gamesa_renderware.hpp +++ b/Client/game_sa/gamesa_renderware.hpp @@ -82,6 +82,7 @@ void InitRwFunctions() RwRasterUnlock = (RwRasterUnlock_t)0x007FAEC0; RwRasterLock = (RwRasterLock_t)0x007FB2D0; RwRasterCreate = (RwRasterCreate_t)0x007FB230; + RwRasterDestroy = (RwRasterDestroy_t)0x007FB020; RwTextureCreate = (RwTextureCreate_t)0x007F37C0; RpMaterialSetTexture = (RpMaterialSetTexture_t)0x0074DBC0; GetAnimHierarchyFromClump = (GetAnimHierarchyFromClump_t)0x734B10; diff --git a/Client/mods/deathmatch/logic/CClientTXD.cpp b/Client/mods/deathmatch/logic/CClientTXD.cpp index 544fa3f293f..51f4b223ec6 100644 --- a/Client/mods/deathmatch/logic/CClientTXD.cpp +++ b/Client/mods/deathmatch/logic/CClientTXD.cpp @@ -97,6 +97,8 @@ bool CClientTXD::Import(unsigned short usModelID) if (!FileLoad(std::nothrow, strUseFilename, m_FileData)) return false; } + // The TXD is used as-is (any format/size). The clothes compositing hooks decompress/resize + // it as needed at composite time, so no up-front normalization is required. m_bUsingFileDataForClothes = true; // Note: ClothesAddReplacement uses the pointer from m_FileData, so don't touch m_FileData until matching ClothesRemove call g_pGame->GetRenderWare()->ClothesAddReplacement(m_FileData.data(), m_FileData.size(), usModelID - CLOTHES_MODEL_ID_FIRST); diff --git a/Client/multiplayer_sa/CMultiplayerSA_ClothesSpeedUp.cpp b/Client/multiplayer_sa/CMultiplayerSA_ClothesSpeedUp.cpp index feb68fa5400..a84fda7b8e8 100644 --- a/Client/multiplayer_sa/CMultiplayerSA_ClothesSpeedUp.cpp +++ b/Client/multiplayer_sa/CMultiplayerSA_ClothesSpeedUp.cpp @@ -83,8 +83,6 @@ namespace int iLastCacheFileId = -1; } // namespace -void OnCrashAverted(uint uiId); - //////////////////////////////////////////////////////////////////////////////////////////////// // // If request is for a file inside player.img (imgId 5) and uiLoadflag is 0 or 1 @@ -317,59 +315,16 @@ bool SetClothingDirectorySize(int directorySize) return true; } -//////////////////////////////////////////////////////////////////////////////////////////////// -// -// CClothesBuilder::CopyTexture null-check hook -// SA CopyTexture at 0x5A5730 derefs the texture parameter without a null -// check. With MTA's fast clothes caching, a cache load failure can leave a TXD dictionary -// invalid. Misc33's hook on RwTexDictionaryFindNamedTexture converts the resulting invalid- -// dictionary crash into a NULL return, which then reaches CopyTexture as a NULL parameter. -// This hook catches that NULL, reports CrashAverted(43), and returns NULL. -// Note: callers of CopyTexture do not null-check the return value, so a crash may still -// occur at a different address. The primary prevention is the loadState verification in -// ShouldSkipLoadRequestedModels; this hook is a last-resort diagnostic. -// -// Original bytes at 0x5A5730 (5 bytes): -// 8B 44 24 04 mov eax, [esp+4] ; get texture param -// 53 push ebx ; save ebx -// (then 8B 18 mov ebx, [eax] ; CRASH when eax=NULL) -// -#define HOOKPOS_CClothesBuilder_CopyTexture 0x5A5730 -#define HOOKSIZE_CClothesBuilder_CopyTexture 5 -static constexpr DWORD RETURN_CClothesBuilder_CopyTexture = 0x5A5735; - -static void _declspec(naked) HOOK_CClothesBuilder_CopyTexture() -{ - MTA_VERIFY_HOOK_LOCAL_SIZE; - - // clang-format off - __asm - { - mov eax, [esp+4] // texture parameter - test eax, eax - jz copytex_null - - // Replicate overwritten instructions and continue - push ebx - jmp RETURN_CClothesBuilder_CopyTexture - - copytex_null: - pushad - push 43 - call OnCrashAverted - add esp, 4 - popad - xor eax, eax // return NULL - ret - } - // clang-format on -} - ////////////////////////////////////////////////////////////////////////////////////////// // // Setup hooks for ClothesSpeedUp // ////////////////////////////////////////////////////////////////////////////////////////// +// Note: the CClothesBuilder::CopyTexture (0x5A5730) null-check that used to live here has +// been removed. game_sa now fully reimplements CopyTexture (and the texture compositing +// primitives) in CRenderWareSA.ClothesTextures.cpp to lift the clothes size limit; that +// replacement includes the same NULL guard, so a second hook at the same address here would +// conflict. void CMultiplayerSA::InitHooks_ClothesSpeedUp() { SetClothingDirectorySize(2050); @@ -377,5 +332,4 @@ void CMultiplayerSA::InitHooks_ClothesSpeedUp() EZHookInstall(CStreamingLoadRequestedModels); EZHookInstall(LoadingPlayerImgDir); EZHookInstall(CallCStreamingInfoAddToList); - EZHookInstall(CClothesBuilder_CopyTexture); } diff --git a/Client/sdk/core/CGraphicsInterface.h b/Client/sdk/core/CGraphicsInterface.h index 6a7edd5ec5c..95f89916bf0 100644 --- a/Client/sdk/core/CGraphicsInterface.h +++ b/Client/sdk/core/CGraphicsInterface.h @@ -201,5 +201,5 @@ class CGraphicsInterface // Texture data manipulation virtual bool ResizeTextureData(const void* pData, uint uiDataPitch, uint uiWidth, uint uiHeight, uint d3dFormat, uint uiNewWidth, uint uiNewHeight, - CBuffer& outBuffer) = 0; + CBuffer& outBuffer, uint d3dNewFormat = 0) = 0; };