From 5a35c1c0f8ded541d2a217c8601e7f419de4e547 Mon Sep 17 00:00:00 2001 From: Caner Karaca Date: Fri, 2 Oct 2026 04:55:20 +0300 Subject: [PATCH] fix(dirtfx): support IVF dirt overlays and paintjobs safely --- src/features/dirtfx.cpp | 479 +++++++++++++++++++++++++++++++++---- src/features/dirtfx.h | 34 ++- src/features/remap.cpp | 5 +- src/utils/modelinfomgr.cpp | 5 +- 4 files changed, 464 insertions(+), 59 deletions(-) diff --git a/src/features/dirtfx.cpp b/src/features/dirtfx.cpp index 7d0f5490..4082c900 100755 --- a/src/features/dirtfx.cpp +++ b/src/features/dirtfx.cpp @@ -1,12 +1,220 @@ #include "pch.h" #include "dirtfx.h" #include "utils/texmgr.h" +#include "utils/modelinfomgr.h" +#include "utils/frameextension.h" +#include +#include +#include using namespace plugin; static CdeclEvent, PRIORITY_AFTER, ArgPickNone, void()> CCarFXRenderer__ShutdownEvent; static CdeclEvent, PRIORITY_AFTER, ArgPickNone, void()> CCarFXRenderer__InitialiseDirtTextureEvent; +using DirtImage = std::unique_ptr; +static constexpr int DirtPixelsPerTick = 131072; + +// SA's RwImageSetFromRaster rejects compressed rasters. Read their native +// D3D9 format (not cFormat, which describes the pixel layout) in small chunks. +static bool ReadDirtImage(RwTexture *texture, DirtImage &image, int &nextRow) +{ + if (!texture || !texture->raster) return false; + RwRaster *raster = texture->raster; + if (raster->width <= 0 || raster->height <= 0) return false; + if (!image) + { + image.reset(RwImageCreate(raster->width, raster->height, 32)); + if (!image || !RwImageAllocatePixels(image.get())) return false; + } + if (image->width != raster->width || image->height != raster->height) return false; + if (!RwD3D9RasterIsCompressed(raster)) + { + if (!RwImageSetFromRaster(image.get(), raster)) return false; + nextRow = raster->height; + return true; + } + + // The D3D9 device raster plugin starts with its native texture pointer. + int offset = RwRasterGetPluginOffset(rwID_DEVICEMODULE); + if (offset < 0) return false; + auto *native = *reinterpret_cast(reinterpret_cast(raster) + offset); + if (!native || native->GetType() != D3DRTYPE_TEXTURE) return false; + auto *d3dTexture = static_cast(native); + D3DSURFACE_DESC desc{}; + if (FAILED(d3dTexture->GetLevelDesc(0, &desc)) || + desc.Width != static_cast(raster->width) || desc.Height != static_cast(raster->height) || + (desc.Format != D3DFMT_DXT1 && desc.Format != D3DFMT_DXT3 && desc.Format != D3DFMT_DXT5)) + return false; + D3DLOCKED_RECT locked{}; + if (FAILED(d3dTexture->LockRect(0, &locked, nullptr, D3DLOCK_READONLY))) return false; + const int blockSize = desc.Format == D3DFMT_DXT1 ? 8 : 16; + if (!locked.pBits || locked.Pitch < ((raster->width + 3) / 4) * blockSize) + { + d3dTexture->UnlockRect(0); + return false; + } + const int lastRow = std::min(raster->height, nextRow + std::max(4, (DirtPixelsPerTick / raster->width) & ~3)); + for (int by = nextRow; by < lastRow; by += 4) + { + for (int bx = 0; bx < raster->width; bx += 4) + { + const auto *block = static_cast(locked.pBits) + (by / 4) * locked.Pitch + (bx / 4) * blockSize; + const auto *colorBlock = block + blockSize - 8; + RwUInt16 endpoints[2]; + RwUInt32 indices; + std::memcpy(endpoints, colorBlock, sizeof(endpoints)); + std::memcpy(&indices, colorBlock + 4, sizeof(indices)); + RwRGBA colors[4]{}; + for (int c = 0; c < 2; ++c) + { + const int r = endpoints[c] >> 11, g = (endpoints[c] >> 5) & 63, b = endpoints[c] & 31; + colors[c] = {static_cast((r << 3) | (r >> 2)), static_cast((g << 2) | (g >> 4)), + static_cast((b << 3) | (b >> 2)), 255}; + } + const bool fourColors = desc.Format != D3DFMT_DXT1 || endpoints[0] > endpoints[1]; + for (int c = 2; c < (fourColors ? 4 : 3); ++c) + { + const int a = fourColors ? 4 - c : 1, b = fourColors ? c - 1 : 1, divisor = a + b; + colors[c] = {static_cast((a * colors[0].red + b * colors[1].red) / divisor), + static_cast((a * colors[0].green + b * colors[1].green) / divisor), + static_cast((a * colors[0].blue + b * colors[1].blue) / divisor), 255}; + } + RwUInt8 alphas[8] = {block[0], block[1], 0, 0, 0, 0, 0, 255}; + uint64_t alphaBits = 0; + if (desc.Format == D3DFMT_DXT5) + { + const int divisor = alphas[0] > alphas[1] ? 7 : 5; + for (int a = 1; a < divisor; ++a) + alphas[a + 1] = static_cast(((divisor - a) * alphas[0] + a * alphas[1]) / divisor); + for (int a = 0; a < 6; ++a) alphaBits |= static_cast(block[a + 2]) << (8 * a); + } + else if (desc.Format == D3DFMT_DXT3) std::memcpy(&alphaBits, block, sizeof(alphaBits)); + for (int y = 0; y < 4 && by + y < raster->height; ++y) + { + auto *row = reinterpret_cast(image->cpPixels + (by + y) * image->stride); + for (int x = 0; x < 4 && bx + x < raster->width; ++x) + { + const int pixel = y * 4 + x; + RwRGBA color = colors[(indices >> (2 * pixel)) & 3]; + if (desc.Format == D3DFMT_DXT3) color.alpha = static_cast(((alphaBits >> (4 * pixel)) & 15) * 17); + if (desc.Format == D3DFMT_DXT5) color.alpha = alphas[(alphaBits >> (3 * pixel)) & 7]; + row[bx + x] = color; + } + } + } + } + d3dTexture->UnlockRect(0); + nextRow = lastRow; + return true; +} + +static RwTexture *FindDirtTexture(RwTexture *clean, bool &overlay) +{ + if (!clean || !clean->dict) return nullptr; + char name[32]; + size_t length = strnlen(clean->name, sizeof(name)); + // Keep the existing _dt replacement convention ahead of IVF alpha overlays. + for (int pass = 0; pass < 2; ++pass) + { + for (const char *suffix : {"_dt", "_dirt", "_d"}) + { + const size_t suffixLength = std::strlen(suffix); + if (length + suffixLength >= sizeof(name) || (pass && std::strcmp(suffix, "_dt") == 0)) continue; + std::memcpy(name, clean->name, length); + std::memcpy(name + length, suffix, suffixLength + 1); + RwTexture *dirt = RwTexDictionaryFindNamedTexture(clean->dict, name); + if (!dirt && _strnicmp(name, "#emap", 5) == 0) + { + name[0] = 'r'; // SA marks vanilla paintjob textures with '#'. + dirt = RwTexDictionaryFindNamedTexture(clean->dict, name); + } + if (dirt) + { + overlay = std::strcmp(suffix, "_dt") != 0; + return dirt; + } + } + // A random remap may share its base's IVF overlay. A complete _dt image + // cannot be shared: that would replace the selected livery at full dirt. + size_t remap = 0; + while (remap + 6 <= length && _strnicmp(clean->name + remap, "_remap", 6) != 0) ++remap; + if (remap == 0 || remap + 6 > length || clean->name[0] == '#') break; + length = remap; + } + return nullptr; +} + +// Only one preparation job owns temporary images; no GPU lock crosses ticks. +static struct DirtJob +{ + RwTexture *source = nullptr, *layer = nullptr; + RwRaster *sourceRaster = nullptr, *layerRaster = nullptr; + int level = 0, phase = 0, row = 0, width = 0, height = 0; + int flags = 0; + size_t bytes = 0; + bool overlay = false, hasAlpha = false; + DirtImage clean{nullptr, RwImageDestroy}, dirt{nullptr, RwImageDestroy}, blended{nullptr, RwImageDestroy}; +} g_DirtJob; + +static void CancelDirtJob() +{ + g_DirtJob = DirtJob{}; +} + +static size_t DirtRasterBytes(int width, int height, bool mipmaps) +{ + size_t bytes = 0; + do + { + bytes += static_cast(width) * height * 4; // D3D9 RGB rasters also use 32-bit storage. + if (!mipmaps || (width == 1 && height == 1)) break; + width = std::max(1, width / 2); + height = std::max(1, height / 2); + } while (true); + return bytes; +} + +void DirtFx::ReleaseDirtStage(DirtStages &stages, int level) +{ + if (RwTexture *texture = stages.textures[level]) + { + stages.textures[level] = nullptr; + m_nCustomCacheBytes -= stages.bytes[level]; + stages.bytes[level] = 0; + RwTextureDestroy(texture); + } + stages.requested &= static_cast(~(1u << level)); + stages.attempted &= static_cast(~(1u << level)); + stages.lastUsed[level] = 0; +} + +void DirtFx::DestroyDirtStages(DirtStages &stages) +{ + for (int level = 1; level < 16; ++level) ReleaseDirtStage(stages, level); +} + +void DirtFx::RegisterVehicleTextures(int model) +{ + if (!m_bEnabled || !m_bCustomReady) return; + auto *info = CModelInfo::GetModelInfo(model); + if (!info || info->m_nTxdIndex < 0 || !CTxdStore::ms_pTxdPool) return; + auto *def = CTxdStore::ms_pTxdPool->GetAt(info->m_nTxdIndex); + if (!def || !def->m_pRwDictionary) return; + RwTexDictionaryForAllTextures(def->m_pRwDictionary, [](RwTexture *texture, void *) + { + bool overlay = false; + if (RwTexture *dirt = FindDirtTexture(texture, overlay)) InitialiseBlendTextureSingleEx(texture, dirt, overlay); + return texture; + }, nullptr); +} + +void DirtFx::Reload(CVehicle *vehicle) +{ + ReloadConfig(); + if (vehicle) RegisterVehicleTextures(vehicle->m_nModelIndex); +} + void DirtFx::ReloadConfig() { CBaseFeature::ReloadConfig(); @@ -16,6 +224,38 @@ void DirtFx::ReloadConfig() void DirtFx::Init() { ReloadConfig(); + Events::attachRwPluginsEvent += []() + { + // Release cached copies when either source streams out; pointer reuse must + // never give another model's textures an old dirt cache. + m_bCustomReady = RwTextureRegisterPlugin(0, PLUGIN_ID_NUM, nullptr, + [](void *object, RwInt32, RwInt32) -> void * + { + auto *texture = static_cast(object); + if (texture == g_DirtJob.source || texture == g_DirtJob.layer) CancelDirtJob(); + for (auto it = m_DirtTextures.begin(); it != m_DirtTextures.end();) + { + if (it->first == texture) + { + auto sources = std::move(it->second); + it = m_DirtTextures.erase(it); + for (auto &[source, stages] : sources) DestroyDirtStages(stages); + } + else + { + auto source = it->second.find(texture); + if (source != it->second.end()) + { + auto stages = source->second; + it->second.erase(source); + DestroyDirtStages(stages); + } + ++it; + } + } + return object; + }, nullptr) >= 0; + }; if (injector::GetBranchDestination(0x6D0E7E).as_int() != 0x5D5DB0) LOG(ERROR) << "Address conflict on 0x6D0E7E"; patch::Nop(0x6D0E7E, 5); @@ -26,71 +266,112 @@ void DirtFx::Init() CCarFXRenderer__ShutdownEvent.after += DirtFx::ShutdownHook; CCarFXRenderer__InitialiseDirtTextureEvent.after += DirtFx::InitialiseDirtTextures; - Events::vehicleSetModelEvent.after += [](CVehicle *pVeh, int model) + Events::vehicleSetModelEvent.after += [](CVehicle *vehicle, int model) { - CVehicleModelInfo *pInfo = (CVehicleModelInfo *)CModelInfo::GetModelInfo(model); - CTxdStore::PushCurrentTxd(); - CTxdStore::SetCurrentTxd(pInfo->m_nTxdIndex); - RwTexDictionaryForAllTextures(RwTexDictionaryGetCurrent(), [](RwTexture *texture, void *pData) - { - RwTexture *pDirtTex = texture; - std::string dirtName = pDirtTex->name; - - if (dirtName.starts_with("#") || dirtName.starts_with("remap") || !dirtName.ends_with("_dt")) { - return texture; - } - - std::string cleanName = dirtName.substr(0, dirtName.find_last_of('_')); - RwTexture *pCleanTex = TextureMgr::FindInDict(cleanName, RwTexDictionaryGetCurrent()); + if (vehicle) RegisterVehicleTextures(model); + }; - if (pCleanTex) { - InitialiseBlendTextureSingleEx(pCleanTex, pDirtTex); + // Vanilla loads selected paintjobs later than the vehicle model. Register + // their sources on the script tick, never inside the material render callback. + Events::processScriptsEvent += []() + { + if (!m_bEnabled || !m_bCustomReady || !CPools::ms_pVehiclePool) return; + for (CVehicle *vehicle : CPools::ms_pVehiclePool) + { + if (!vehicle || !vehicle->m_pRemapTexture) continue; + bool overlay = false; + if (RwTexture *dirt = FindDirtTexture(vehicle->m_pRemapTexture, overlay)) + { + InitialiseBlendTextureSingleEx(vehicle->m_pRemapTexture, dirt, overlay); + continue; + } + auto *info = CModelInfo::GetModelInfo(vehicle->m_nModelIndex); + if (!info || info->m_nTxdIndex < 0 || !CTxdStore::ms_pTxdPool) continue; + auto *def = CTxdStore::ms_pTxdPool->GetAt(info->m_nTxdIndex); + if (!def || !def->m_pRwDictionary) continue; + for (auto &[dirt, sources] : m_DirtTextures) + { + if (dirt->dict != def->m_pRwDictionary || !std::any_of(sources.begin(), sources.end(), + [](const auto &source) { return source.first->name[0] == '#'; })) continue; + const size_t length = std::strlen(dirt->name); + overlay = length < 3 || _stricmp(dirt->name + length - 3, "_dt") != 0; + if (overlay) InitialiseBlendTextureSingleEx(vehicle->m_pRemapTexture, dirt, true); } - return texture; - }, NULL); - CTxdStore::PopCurrentTxd(); + } + // Finish a bounded part of one job per tick; render callbacks only request levels. + if (g_DirtJob.source) + { + ProcessDirtJob(); + return; + } + for (auto &[dirt, sources] : m_DirtTextures) + { + for (auto &[source, stages] : sources) + { + for (int level = 1; level < 16; ++level) + { + if ((stages.requested & ~stages.attempted) & (1u << level)) + { + InitialiseDirtStage(source, dirt, stages, level); + return; + } + } + } + } }; } -void DirtFx::ProcessTextures(CVehicle *pVeh, RpMaterial *pMat) { - if (!m_bEnabled || !pMat || !pMat->texture || !pMat->texture->name) { +void DirtFx::ProcessTextures(CVehicle *pVeh, RpMaterial *pMat, RwTexture *baseTexture) { + if (!m_bEnabled || !pVeh || !pMat || !pMat->texture) { return; } const char *rawName = pMat->texture->name; char first = rawName[0]; - int dirtLvl = std::clamp(static_cast(pVeh->m_fDirtLevel), 0, 15); + const float dirtLevel = std::isfinite(pVeh->m_fDirtLevel) ? pVeh->m_fDirtLevel : 0.0f; + int dirtLvl = static_cast(std::clamp(dirtLevel, 0.0f, 15.0f)); + RwTexture *target = nullptr; if (first == 'v') { std::string_view texName(rawName); if (texName == "vehiclegrunge256") { - RpMaterialSetTexture(pMat, ms_aDirtTextures[dirtLvl]); - return; + target = ms_aDirtTextures[dirtLvl]; } if (texName == "vehicle_genericmud_truck" || texName == "vehiclegrunge_iv") { - RpMaterialSetTexture(pMat, ms_aDirtTextures_2[dirtLvl]); - return; + target = ms_aDirtTextures_2[dirtLvl]; } if (texName == "vehiclegrunge512") { - RpMaterialSetTexture(pMat, ms_aDirtTextures_3[dirtLvl]); - return; + target = ms_aDirtTextures_3[dirtLvl]; } } else if (first == 't') { std::string_view texName(rawName); if (texName.starts_with("tyrewall_dirt")) { - RpMaterialSetTexture(pMat, ms_aDirtTextures_4[dirtLvl]); - return; + target = ms_aDirtTextures_4[dirtLvl]; } } - if (!m_DirtTextures.empty()) + if (!target && dirtLvl > 0 && !m_DirtTextures.empty()) { - auto it = m_DirtTextures.find(rawName); + bool overlay = false; + RwTexture *dirt = FindDirtTexture(pMat->texture, overlay); + if (!dirt && baseTexture) dirt = FindDirtTexture(baseTexture, overlay); + auto it = m_DirtTextures.find(dirt); if (it != m_DirtTextures.end()) { - RpMaterialSetTexture(pMat, it->second[dirtLvl]); + auto source = it->second.find(pMat->texture); + if (source != it->second.end()) + { + source->second.requested |= static_cast(1u << dirtLvl); + target = source->second.textures[dirtLvl]; + if (target) source->second.lastUsed[dirtLvl] = ++m_nTextureUse; + } } } + if (target && target != pMat->texture) + { + ModelInfoMgr::RegisterRestore(&pMat->texture, pMat->texture); + pMat->texture = target; + } } void DirtFx::Shutdown() @@ -101,6 +382,11 @@ void DirtFx::Shutdown() void DirtFx::ShutdownHook() { + CancelDirtJob(); + auto custom = std::move(m_DirtTextures); + m_DirtTextures.clear(); + for (auto &[dirt, sources] : custom) + for (auto &[source, stages] : sources) DestroyDirtStages(stages); for (int i = 0; i < 16; i++) { if (ms_aDirtTextures_2[i]) RwTextureDestroy(ms_aDirtTextures_2[i]); @@ -109,22 +395,122 @@ void DirtFx::ShutdownHook() } } -void DirtFx::InitialiseBlendTextureSingleEx(RwTexture *src, RwTexture *dest) +void DirtFx::InitialiseBlendTextureSingleEx(RwTexture *src, RwTexture *dest, bool overlay) +{ + if (!m_bCustomReady || !src || !dest || !src->raster || !dest->raster) return; + m_DirtTextures[dest].try_emplace(src).first->second.overlay = overlay; +} + +void DirtFx::InitialiseDirtStage(RwTexture *src, RwTexture *dest, DirtStages &stages, int level) +{ + stages.attempted |= static_cast(1u << level); + if (!src || !dest || !src->raster || !dest->raster) return; + const int width = stages.overlay ? std::max(src->raster->width, dest->raster->width) : src->raster->width; + const int height = stages.overlay ? std::max(src->raster->height, dest->raster->height) : src->raster->height; + // Reject oversized/invalid inputs before allocating temporary images. + for (RwTexture *texture : {src, dest}) + if (texture->raster->width <= 0 || texture->raster->height <= 0 || + static_cast(texture->raster->width) * texture->raster->height * 4 > CustomCacheLimit) return; + if (static_cast(width) * height * 4 > CustomCacheLimit) return; + const size_t bytes = DirtRasterBytes(width, height, (RwRasterGetFormat(src->raster) & rwRASTERFORMATMIPMAP) != 0); + if (bytes > CustomCacheLimit) return; + g_DirtJob.source = src; + g_DirtJob.layer = dest; + g_DirtJob.sourceRaster = src->raster; + g_DirtJob.layerRaster = dest->raster; + g_DirtJob.level = level; + g_DirtJob.width = width; + g_DirtJob.height = height; + g_DirtJob.overlay = stages.overlay; + g_DirtJob.flags = rwRASTERTYPETEXTURE | (RwRasterGetFormat(src->raster) & rwRASTERFORMATMIPMAP); + g_DirtJob.bytes = bytes; +} + +void DirtFx::ProcessDirtJob() { - if (src && dest && !m_DirtTextures.contains(src->name)) + auto &job = g_DirtJob; + if (!job.source || !job.layer || job.source->raster != job.sourceRaster || job.layer->raster != job.layerRaster) { - src->filterAddressing = rwFILTERLINEAR; - dest->filterAddressing = rwFILTERLINEAR; - for (size_t i = 0; i < 16; i++) + CancelDirtJob(); + return; + } + if (job.phase < 2) + { + DirtImage &image = job.phase == 0 ? job.clean : job.dirt; + if (!ReadDirtImage(job.phase == 0 ? job.source : job.layer, image, job.row)) { - float FacB = (1.0f / 15.0f) * static_cast(i); - float FacA = 1.0f - FacB; - RwTexture *pTex = CClothesBuilder::CopyTexture(src); - RwTextureSetName(pTex, src->name); - CClothesBuilder::BlendTextures(pTex, dest, FacA, FacB); - m_DirtTextures[src->name].push_back(pTex); + CancelDirtJob(); + return; } + if (job.row == image->height) { ++job.phase; job.row = 0; } + return; } + const int width = job.overlay ? std::max(job.clean->width, job.dirt->width) : job.clean->width; + const int height = job.overlay ? std::max(job.clean->height, job.dirt->height) : job.clean->height; + if (width != job.width || height != job.height) { CancelDirtJob(); return; } + if (job.phase == 2) + { + job.blended.reset(RwImageCreate(width, height, 32)); + if (!job.blended || !RwImageAllocatePixels(job.blended.get())) { CancelDirtJob(); return; } + ++job.phase; + return; + } + if (job.phase == 3) + { + const float factor = static_cast(job.level) / 15.0f; + const int lastRow = std::min(height, job.row + std::max(1, DirtPixelsPerTick / width)); + for (int y = job.row; y < lastRow; ++y) + { + const auto *cleanRow = reinterpret_cast(job.clean->cpPixels + (static_cast(y) * job.clean->height / height) * job.clean->stride); + const auto *dirtRow = reinterpret_cast(job.dirt->cpPixels + (static_cast(y) * job.dirt->height / height) * job.dirt->stride); + auto *out = reinterpret_cast(job.blended->cpPixels + y * job.blended->stride); + for (int x = 0; x < width; ++x) + { + const RwRGBA &a = cleanRow[static_cast(x) * job.clean->width / width], &b = dirtRow[static_cast(x) * job.dirt->width / width]; + const float weight = job.overlay ? factor * b.alpha / 255.0f : factor; + out[x] = {static_cast(a.red * (1.0f - weight) + b.red * weight), + static_cast(a.green * (1.0f - weight) + b.green * weight), + static_cast(a.blue * (1.0f - weight) + b.blue * weight), + job.overlay ? a.alpha : static_cast(a.alpha * (1.0f - factor) + b.alpha * factor)}; + job.hasAlpha |= out[x].alpha != 255; + } + } + job.row = lastRow; + if (job.row == height) ++job.phase; + return; + } + auto &stages = m_DirtTextures.at(job.layer).at(job.source); + // Evict the least recently rendered copies before allocating the new raster. + while (m_nCustomCacheBytes > CustomCacheLimit - job.bytes) + { + DirtStages *oldest = nullptr; + int oldLevel = 0; + uint64_t age = UINT64_MAX; + for (auto &[dirt, sources] : m_DirtTextures) + for (auto &[source, cached] : sources) + for (int level = 1; level < 16; ++level) + if (cached.textures[level] && cached.lastUsed[level] < age) + { oldest = &cached; oldLevel = level; age = cached.lastUsed[level]; } + if (!oldest) { CancelDirtJob(); return; } + ReleaseDirtStage(*oldest, oldLevel); + } + RwRaster *raster = RwRasterCreate(width, height, job.hasAlpha ? 32 : 24, + job.flags | (job.hasAlpha ? rwRASTERFORMAT8888 : rwRASTERFORMAT888)); + RwTexture *texture = nullptr; + if (raster && RwRasterSetFromImage(raster, job.blended.get()) && + (!(job.flags & rwRASTERFORMATMIPMAP) || RwTextureRasterGenerateMipmaps(raster, job.blended.get()))) + texture = RwTextureCreate(raster); + if (texture) + { + RwTextureSetName(texture, job.source->name); + texture->filterAddressing = job.source->filterAddressing; + stages.textures[job.level] = texture; + stages.lastUsed[job.level] = ++m_nTextureUse; + stages.bytes[job.level] = job.bytes; + m_nCustomCacheBytes += job.bytes; + } + else if (raster) RwRasterDestroy(raster); + CancelDirtJob(); } void DirtFx::InitialiseBlendTextureSingle(const char *CleanName, const char *DirtName, RwTexture **TextureArray) @@ -202,5 +588,4 @@ void DirtFx::InitialiseDirtTextures() // Textures which belnd between two images InitialiseBlendTextureSingle("tyrewall_dirt", "tyrewall_dirt_dt", ms_aDirtTextures_4); - // InitialiseBlendTextureSingle((char *)"vehicle_generic_detail", (char *)"vehicle_generic_detaild", ms_aDirtTextures_5); -} \ No newline at end of file +} diff --git a/src/features/dirtfx.h b/src/features/dirtfx.h index 1ee912bc..345c61fb 100755 --- a/src/features/dirtfx.h +++ b/src/features/dirtfx.h @@ -3,9 +3,8 @@ #include "core/base.h" #include "CTxdStore.h" #include "CClothesBuilder.h" -#include #include -#include +#include using namespace plugin; @@ -19,23 +18,40 @@ class DirtFx : public CBaseFeature static inline RwTexture *ms_aDirtTextures_2[16] = {}; static inline RwTexture *ms_aDirtTextures_3[16] = {}; static inline RwTexture *ms_aDirtTextures_4[16] = {}; - static inline RwTexture *ms_aDirtTextures_5[16] = {}; - static inline RwTexture *ms_aDirtTextures_6[16] = {}; - static inline std::unordered_map> m_DirtTextures; + // Key by the actual dirt/clean textures, not names shared by unrelated TXDs. + struct DirtStages + { + std::array textures{}; + RwUInt16 requested = 0; + RwUInt16 attempted = 0; + bool overlay = false; + std::array bytes{}; + std::array lastUsed{}; + }; + static inline std::map> m_DirtTextures; + static inline bool m_bCustomReady = false; + static constexpr size_t CustomCacheLimit = 128u * 1024u * 1024u; + static inline size_t m_nCustomCacheBytes = 0; + static inline uint64_t m_nTextureUse = 0; void Shutdown() override; static void ShutdownHook(); static void InitialiseDirtTextures(); static void InitialiseBlendTextureSingle(const char *CleanName, const char *DirtName, RwTexture **TextureArray); - static void InitialiseBlendTextureSingleEx(RwTexture *src, RwTexture *dest); + static void InitialiseBlendTextureSingleEx(RwTexture *src, RwTexture *dest, bool overlay); + static void InitialiseDirtStage(RwTexture *src, RwTexture *dest, DirtStages &stages, int level); + static void ProcessDirtJob(); + static void ReleaseDirtStage(DirtStages &stages, int level); + static void DestroyDirtStages(DirtStages &stages); + static void RegisterVehicleTextures(int model); static void InitialiseDirtTextureSingle(const char *name, RwTexture **Array); protected: void Init() override; void ReloadConfig() override; - void Reload(CVehicle *pVeh) override { ReloadConfig(); } + void Reload(CVehicle *pVeh) override; public: DirtFx() : CBaseFeature("DirtFX", "FEATURES", eFeatureMatrix::DirtFX) {} - static void ProcessTextures(CVehicle *pVeh, RpMaterial *pMat); -}; \ No newline at end of file + static void ProcessTextures(CVehicle *pVeh, RpMaterial *pMat, RwTexture *baseTexture = nullptr); +}; diff --git a/src/features/remap.cpp b/src/features/remap.cpp index d78ef8fe..a957a873 100755 --- a/src/features/remap.cpp +++ b/src/features/remap.cpp @@ -65,7 +65,8 @@ void Remap::LoadRemaps(CVehicle* vehicle) // Group textures into base -> [base, remap1, remap2, ...] for (const auto &[name, pTex] : allTextures) { - if (name.starts_with("#") || name.starts_with("remap")) + if (name.starts_with("#") || name.starts_with("remap") || + name.ends_with("_dt") || name.ends_with("_dirt") || name.ends_with("_d")) continue; std::size_t remapPos = name.find("_remap"); @@ -144,4 +145,4 @@ void Remap::ProcessTextures(CVehicle *pVeh, RpMaterial *pMat) ModelInfoMgr::RegisterRestore(&pMat->texture, pMat->texture); pMat->texture = it->second[chosen]; } -} \ No newline at end of file +} diff --git a/src/utils/modelinfomgr.cpp b/src/utils/modelinfomgr.cpp index 5e5cbee7..3f0a98d4 100755 --- a/src/utils/modelinfomgr.cpp +++ b/src/utils/modelinfomgr.cpp @@ -350,6 +350,7 @@ RpMaterial *ModelInfoMgr::SetEditableMaterialsCB(RpMaterial *material, return material; } + RwTexture *baseTexture = material->texture; if (material->texture) { const char *texName = material->texture->name; bool isRemapTex = (texName && texName[0] == '#'); @@ -360,7 +361,6 @@ RpMaterial *ModelInfoMgr::SetEditableMaterialsCB(RpMaterial *material, } } else if (pCurVeh) { Remap::ProcessTextures(pCurVeh, material); - DirtFx::ProcessTextures(pCurVeh, material); LicensePlate::ProcessTextures(pCurVeh, material); } } @@ -450,6 +450,9 @@ RpMaterial *ModelInfoMgr::SetEditableMaterialsCB(RpMaterial *material, } } + // Resolve light textures using their original TXD before substituting a + // generated dirt texture, which deliberately has no dictionary. + DirtFx::ProcessTextures(pCurVeh, material, baseTexture); return material; }