diff --git a/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl b/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl index 468d850874e..aa05d764667 100644 --- a/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl +++ b/cont/base/springcontent/shaders/GLSL/ModelVertProgGL4.glsl @@ -262,7 +262,7 @@ Transform Lerp(Transform t0, Transform t1, float a) { void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) { - bool staticModel = (matrixMode > 0); + bool staticModel = (matrixMode == MATMODE_STATIC || matrixMode == MATMODE_ARRAY); vec4 piecePos = vec4(pos, 1.0); vec4 normal4 = vec4(normal, 0.0); @@ -271,7 +271,10 @@ void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) Transform tx; if (staticModel) { - tx = transforms[instData.x + bID0]; + // pieces always come from the bind-pose block (instData.w). In ARRAY_MATMODE + // instData.x is the per-instance world transform, not the bind pose; for static + // model submits instData.x == instData.w anyway, so instData.w is correct for both. + tx = transforms[instData.w + bID0]; } else { // do interpolation tx = Lerp( @@ -327,16 +330,20 @@ void GetModelSpaceVertex(out vec4 msPosition, out vec3 msNormal) void main(void) { - bool staticModel = (matrixMode > 0); - vec4 modelPos; vec3 modelNormal; GetModelSpaceVertex(modelPos, modelNormal); - if (staticModel) { + if (matrixMode == MATMODE_ARRAY) { + // static instanced: per-instance world transform read from the SSBO (no interpolation) + Transform wtx = transforms[instData.x + 0u]; + worldPos = ApplyTransform(wtx, modelPos); + wtx.trSc = vec4(0, 0, 0, 1); //nullify the translation part for the normal + worldNormal = ApplyTransform(wtx, modelNormal); + } else if (matrixMode == MATMODE_STATIC) { worldPos = staticModelMatrix * modelPos; worldNormal = mat3(staticModelMatrix) * modelNormal; - } else { + } else { // MATMODE_NORMAL // do interpolation Transform tx = Lerp( transforms[instData.x + 0u], diff --git a/doc/site/content/changelogs/_index.markdown b/doc/site/content/changelogs/_index.markdown index 1e8e738da3a..0906e9055e3 100644 --- a/doc/site/content/changelogs/_index.markdown +++ b/doc/site/content/changelogs/_index.markdown @@ -57,6 +57,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Added cancelcommand action - Added GetPrevFrameChecksum() to the Lua API [PR 2922](https://github.com/beyond-all-reason/RecoilEngine/pull/2922) - Fixes to Spring.SetMapShader [PR 3127](https://github.com/beyond-all-reason/RecoilEngine/pull/3127) +- MouseHandler: route XButtons (Mouse4/5) as keybinds instead of mouse ownership [PR 2613](https://github.com/beyond-all-reason/RecoilEngine/pull/2613) ## Misc - Restored lowercasing in FileSystem::GetExtension @@ -80,6 +81,7 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fix stale spGetActionHotKeys returns [PR 3082](https://github.com/beyond-all-reason/RecoilEngine/pull/3082) - Use a portable type cast in MemPoolTypes logging - Support alternate file extensions for replays [PR 2975](https://github.com/beyond-all-reason/RecoilEngine/pull/2975) +- Throw error and stop processing if modrules parsing fails ## Rendering - Added sorting icon names before adding to atlas so insertion order is consistent across runs. @@ -96,6 +98,8 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Debug logs for loading splat normals - Fix SMF DNTS gating and fallback textures - Add mapoptions for blank map splats +- Performance improvements with drawing ghosted buildings [PR 3110](https://github.com/beyond-all-reason/RecoilEngine/pull/3110) +- Changed ghosted buildings are drawn based on the last team was seen with and so which team ownership doesn't show automatically on the ghost. [PR 3108](https://github.com/beyond-all-reason/RecoilEngine/pull/3108) ## Simulation - Sanitize NaNs in CHoverAirMoveType::UpdateMoveRate() @@ -106,3 +110,4 @@ This is the bleeding-edge changelog since version 2026.06, for **pre-release 202 - Fixed edge scrolling threshold [Issue 2987](https://github.com/beyond-all-reason/RecoilEngine/issues/2987) - Dump state handles resource packs - Fix units having the wrong path id after loading a save game. [PR 3120](https://github.com/beyond-all-reason/RecoilEngine/pull/3120) +- Avoid UB in float-to-short angle casts (fixes arm64/x86 desync) [PR 3075](https://github.com/beyond-all-reason/RecoilEngine/pull/3075) diff --git a/doc/site/content/changelogs/changelog-2026-06.markdown b/doc/site/content/changelogs/changelog-2026-06.markdown index 993a030f907..90a9f6ee9ec 100644 --- a/doc/site/content/changelogs/changelog-2026-06.markdown +++ b/doc/site/content/changelogs/changelog-2026-06.markdown @@ -1,8 +1,6 @@ +++ -title = "Running changelog" -[cascade] - [cascade.params] - type = "docs" +title = "Release 2026.06" +aliases = ['/changelogs/changelog-2026-06'] +++ This is the bleeding-edge changelog since version 2025.06, for **pre-release 2026.06**. diff --git a/doc/site/mise.toml b/doc/site/mise.toml index 3eac83faf22..e827931ebe8 100644 --- a/doc/site/mise.toml +++ b/doc/site/mise.toml @@ -1,3 +1,6 @@ +[settings.npm] + package_manager = "npm" + [tools] # We only install tools required for each task on each task, unless all tasks use a tool here # For CI when we need to install and cache all tools, see mise.ci.toml @@ -101,7 +104,7 @@ [tasks.lua_library] description = "Generate Lua Docs" - tools."npm:lua-doc-extractor" = "{{env.LUA_DOC_EXTRACTOR_VERSION}}" + tools."npm:lua-doc-extractor" = { version = "{{env.LUA_DOC_EXTRACTOR_VERSION}}", allow_low_downloads = true } dir = "../../" run = [ "lua-doc-extractor --src \"{{vars.lua_doc_paths}}\" --dest {{vars.lua_doc_gen_dest}} --repo \"${LUA_DOC_EXTRACTOR_SOURCE_REF}\"" diff --git a/rts/Game/UI/MiniMap.cpp b/rts/Game/UI/MiniMap.cpp index 2f202bffc18..30037a609c1 100644 --- a/rts/Game/UI/MiniMap.cpp +++ b/rts/Game/UI/MiniMap.cpp @@ -457,7 +457,7 @@ void CMiniMap::ConfigCommand(const std::string& line) const bool wantMaximized = (words.size() >= 2) ? !!atoi(words[1].c_str()) : !isMaximized; if (isMaximized != wantMaximized) - ToggleMaximized(StrCaseStr(words[0].c_str(), "maxspect") == 0); + ToggleMaximized(hashStringLower(words[0].c_str()) != hashString("maxspect")); } break; case hashString("mouseevents"): { diff --git a/rts/Game/UI/MouseHandler.cpp b/rts/Game/UI/MouseHandler.cpp index 400cffa743f..62e155622b7 100644 --- a/rts/Game/UI/MouseHandler.cpp +++ b/rts/Game/UI/MouseHandler.cpp @@ -361,6 +361,24 @@ void CMouseHandler::MousePress(int x, int y, int button) pressedBitMask |= 1 << button; + const bool isXButton = (button == SDL_BUTTON_X1 || button == SDL_BUTTON_X2); + if (isXButton) { + + // 1. Lua first + if (luaInputReceiver->MousePress(x, y, button)) { + return; + } + + // 2. GameInputReceiver via the same path as mouse buttons + auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + if (activeControllerReceiver && activeControllerReceiver->MousePress(x, y, button)) { + // NOTE: X‑buttons bypass ownership so they can be pressed/released without stealing or confusing activeReceiver + return; + } + return; + } + + if (activeReceiver != nullptr && activeReceiver->MousePress(x, y, button)) return; @@ -530,6 +548,22 @@ void CMouseHandler::MouseRelease(int x, int y, int button) return; } + const bool isXButton = (button == SDL_BUTTON_X1 || button == SDL_BUTTON_X2); + if (isXButton) { + + // 1. Lua first + luaInputReceiver->MouseRelease(x, y, button); + + // 2. GameInputReceiver via the same path as mouse buttons + auto activeControllerReceiver = (activeController == nullptr) ? nullptr : activeController->GetInputReceiver(); + if (activeControllerReceiver) { + activeControllerReceiver->MouseRelease(x, y, button); + } + + // 3. Skip ownership funnel + return; + } + if (activeReceiver != nullptr) { activeReceiver->MouseRelease(x, y, button); diff --git a/rts/Rendering/Common/ModelDrawerState.cpp b/rts/Rendering/Common/ModelDrawerState.cpp index b732a8d22c7..3aaa4c8c94c 100644 --- a/rts/Rendering/Common/ModelDrawerState.cpp +++ b/rts/Rendering/Common/ModelDrawerState.cpp @@ -263,6 +263,10 @@ CModelDrawerStateGL4::CModelDrawerStateGL4() modelShaders[n]->SetFlag("GBUFFER_MISCTEX_IDX", GL::GeometryBuffer::ATTACHMENT_MISCTEX); modelShaders[n]->SetFlag("GBUFFER_ZVALTEX_IDX", GL::GeometryBuffer::ATTACHMENT_ZVALTEX); + // name the matrix-mode values the shader compares against, so it reads modes by name + modelShaders[n]->SetFlag("MATMODE_STATIC", static_cast(ShaderMatrixModes::STATIC_MATMODE)); + modelShaders[n]->SetFlag("MATMODE_ARRAY", static_cast(ShaderMatrixModes::ARRAY_MATMODE)); + modelShaders[n]->Link(); modelShaders[n]->Enable(); modelShaders[n]->Disable(); diff --git a/rts/Rendering/Models/3DModelVAO.cpp b/rts/Rendering/Models/3DModelVAO.cpp index 7be3e00733f..ae9d82d799c 100644 --- a/rts/Rendering/Models/3DModelVAO.cpp +++ b/rts/Rendering/Models/3DModelVAO.cpp @@ -286,14 +286,28 @@ void S3DModelVAO::DrawElements(GLenum prim, uint32_t vboIndxStart, uint32_t vboI glDrawElements(prim, vboIndxCount, GL_UNSIGNED_INT, indxVBO.GetPtr(vboIndxStart * sizeof(uint32_t))); } +bool S3DModelVAO::EmplaceInstance(uint32_t indexStart, uint32_t indexCount, uint32_t traIndex, uint16_t paletteIndex, uint16_t numPieces, uint32_t uniIndex, uint32_t bposeIndex) +{ + RECOIL_DETAILED_TRACY_ZONE; + if (traIndex == TransformsMemStorage::INVALID_INDEX || bposeIndex == TransformsMemStorage::INVALID_INDEX) + return false; + + modelDataToInstance[SIndexAndCount{ indexStart, indexCount }].emplace_back(SInstanceData( + traIndex, + paletteIndex, + numPieces, + uniIndex, + bposeIndex + )); + + return true; +} + template bool S3DModelVAO::AddToSubmissionImpl(const TObj* obj, uint32_t indexStart, uint32_t indexCount, uint16_t paletteIndex) { RECOIL_DETAILED_TRACY_ZONE; const auto traIndex = transformsUploader.GetElemOffset(obj); - if (traIndex == TransformsMemStorage::INVALID_INDEX) - return false; - const auto uniIndex = modelUniformsStorage.GetObjOffset(obj); //doesn't need to exist for defs and models. Don't check for validity uint16_t numPieces = 0; @@ -307,19 +321,14 @@ bool S3DModelVAO::AddToSubmissionImpl(const TObj* obj, uint32_t indexStart, uint bposeIndex = transformsUploader.GetElemOffset(obj->model); } - if (bposeIndex == TransformsMemStorage::INVALID_INDEX) - return false; - - auto& modelInstanceData = modelDataToInstance[SIndexAndCount{ indexStart, indexCount }]; - modelInstanceData.emplace_back(SInstanceData( + return EmplaceInstance( + indexStart, indexCount, static_cast(traIndex), paletteIndex, numPieces, static_cast(uniIndex), static_cast(bposeIndex) - )); - - return true; + ); } bool S3DModelVAO::AddToSubmission(const S3DModel* model, uint16_t paletteIndex) @@ -363,6 +372,22 @@ bool S3DModelVAO::AddToSubmission(const UnitDef* unitDef, uint16_t paletteIndex) return AddToSubmissionImpl(unitDef, model->indxStart, model->indxCount, paletteIndex); } +bool S3DModelVAO::AddStaticInstance(const S3DModel* model, uint32_t worldTransformOffset, uint16_t paletteIndex) +{ + RECOIL_DETAILED_TRACY_ZONE; + assert(model); + + // the world transform is the caller-supplied slot; pieces are read from the model bind pose + return EmplaceInstance( + model->indxStart, model->indxCount, + worldTransformOffset, + paletteIndex, + static_cast(model->numPieces), + static_cast(modelUniformsStorage.GetObjOffset(model)), + static_cast(transformsUploader.GetElemOffset(model)) + ); +} + void S3DModelVAO::Submit(GLenum mode, bool bindUnbind) { diff --git a/rts/Rendering/Models/3DModelVAO.hpp b/rts/Rendering/Models/3DModelVAO.hpp index 4a5a6f359d1..e64f3fba517 100644 --- a/rts/Rendering/Models/3DModelVAO.hpp +++ b/rts/Rendering/Models/3DModelVAO.hpp @@ -71,6 +71,9 @@ class S3DModelVAO { bool AddToSubmission(const CFeature* feature); bool AddToSubmission(const UnitDef* unitDef, uint16_t paletteIndex); + + bool AddStaticInstance(const S3DModel* model, uint32_t worldTransformOffset, uint16_t paletteIndex); + void Submit(GLenum mode = GL_TRIANGLES, bool bindUnbind = false); bool SubmitImmediately(const S3DModel* model, uint16_t paletteIndex, GLenum mode = GL_TRIANGLES, bool bindUnbind = false); @@ -102,6 +105,17 @@ class S3DModelVAO { uint32_t indexCount, uint16_t paletteIndex ); + // build one SInstanceData from already-resolved offsets and queue it for the next Submit(); + // returns false (drawing nothing) if the world transform or bind pose is unavailable. + bool EmplaceInstance( + uint32_t indexStart, + uint32_t indexCount, + uint32_t traIndex, + uint16_t paletteIndex, + uint16_t numPieces, + uint32_t uniIndex, + uint32_t bposeIndex + ); void EnableAttribs(bool inst) const; void DisableAttribs() const; private: diff --git a/rts/Rendering/Units/UnitDrawer.cpp b/rts/Rendering/Units/UnitDrawer.cpp index a439038dabe..9a99cdf6d09 100644 --- a/rts/Rendering/Units/UnitDrawer.cpp +++ b/rts/Rendering/Units/UnitDrawer.cpp @@ -2,6 +2,8 @@ #include "UnitDrawer.h" +#include + #include "Game/Camera.h" #include "Game/CameraHandler.h" #include "Game/Game.h" @@ -822,7 +824,7 @@ void CUnitDrawerGLSL::DrawAlphaObjects(int modelType, bool drawReflection, bool CModelDrawerHelper::BindModelTypeTexture(modelType, mdlRenderer.GetObjectBinKey(i)); for (auto* o : mdlRenderer.GetObjectBin(i)) { - DrawAlphaUnit(o, modelType, thisPassMask, false); + DrawAlphaUnit(o, thisPassMask); } } @@ -869,66 +871,35 @@ void CUnitDrawerGLSL::DrawGhostedBuildings(int modelType) const glColor4f(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); // buildings that died while ghosted - for (GhostSolidObject* dgb : deadGhostedBuildings) { - if (camera->InView(dgb->pos, dgb->GetModel()->GetDrawRadius())) { - glPushMatrix(); - glTranslatef3(dgb->pos); - glRotatef(dgb->facing * 90.0f, 0, 1, 0); + for (const GhostSolidObject* dgb : deadGhostedBuildings) { + const S3DModel* model = dgb->GetModel(); + if (!camera->InView(dgb->pos, model->GetDrawRadius())) + continue; - CModelDrawerHelper::BindModelTypeTexture(modelType, dgb->GetModel()->textureType); - SetTeamColor(dgb->team, IModelDrawerState::alphaValues.y); + glPushMatrix(); + glTranslatef3(dgb->pos); + glRotatef(dgb->facing * 90.0f, 0, 1, 0); - dgb->GetModel()->DrawStatic(); - glPopMatrix(); - } - } + CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); + SetTeamColor(dgb->team, IModelDrawerState::alphaValues.y); - for (CUnit* lgb : liveGhostedBuildings) { - DrawAlphaUnit(lgb, modelType, DrawFlags::SO_ALPHAF_FLAG, true); + model->DrawStatic(); + glPopMatrix(); } -} - -void CUnitDrawerGLSL::DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (!ShouldDrawOpaqueUnit(unit, thisPassMask)) - return; - - // draw the unit with the default (non-Lua) material - SetTeamColor(unit->team); - DrawUnitTrans(unit, 0, 0, false, false); -} - -void CUnitDrawerGLSL::DrawUnitShadow(CUnit* unit) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (ShouldDrawUnitShadow(unit)) - DrawUnitTrans(unit, 0, 0, false, false); -} -void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPassMask, bool drawGhostBuildingsPass) const -{ - RECOIL_DETAILED_TRACY_ZONE; - if (!drawGhostBuildingsPass && !ShouldDrawAlphaUnit(unit, thisPassMask)) - return; + // buildings that left LOS but are still alive + for (const auto& lgb : liveGhostedBuildings) { + const CUnit* unit = lgb.unit; - const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; - - if (drawGhostBuildingsPass) { // check for decoy models const UnitDef* decoyDef = unit->unitDef->decoyDef; - const S3DModel* model = nullptr; - - if (decoyDef == nullptr) { - model = unit->model; - } - else { - model = decoyDef->LoadModel(); - } + const S3DModel* model = (decoyDef == nullptr) ? unit->model : decoyDef->LoadModel(); // FIXME: needs a second pass if (model->type != modelType) - return; + continue; + + const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; // ghosted enemy units if (losStatus & LOS_CONTRADAR) { @@ -948,17 +919,45 @@ void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPass // not actually cloaked CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); - SetTeamColor(unit->team, (losStatus & LOS_CONTRADAR) ? IModelDrawerState::alphaValues.z : IModelDrawerState::alphaValues.y); + // color with the team the unit was last seen under, not the live unit's current team + const float ghostAlpha = (losStatus & LOS_CONTRADAR) ? IModelDrawerState::alphaValues.z : IModelDrawerState::alphaValues.y; + SetTeamColor(lgb.team, ghostAlpha); model->DrawStatic(); glPopMatrix(); glColor4f(1.0f, 1.0f, 1.0f, IModelDrawerState::alphaValues.x); - return; } +} + +void CUnitDrawerGLSL::DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (!ShouldDrawOpaqueUnit(unit, thisPassMask)) + return; + + // draw the unit with the default (non-Lua) material + SetTeamColor(unit->team); + DrawUnitTrans(unit, 0, 0, false, false); +} + +void CUnitDrawerGLSL::DrawUnitShadow(CUnit* unit) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (ShouldDrawUnitShadow(unit)) + DrawUnitTrans(unit, 0, 0, false, false); +} + +void CUnitDrawerGLSL::DrawAlphaUnit(CUnit* unit, uint8_t thisPassMask) const +{ + RECOIL_DETAILED_TRACY_ZONE; + if (!ShouldDrawAlphaUnit(unit, thisPassMask)) + return; if (unit->GetIsIcon()) return; + const unsigned short losStatus = unit->losStatus[gu->myAllyTeam]; + if ((losStatus & LOS_INLOS) || gu->spectatingFullView) { SetTeamColor(unit->team, IModelDrawerState::alphaValues.x); DrawUnitTrans(unit, 0, 0, false, false); @@ -1738,92 +1737,119 @@ void CUnitDrawerGL4::DrawAlphaObjects(int modelType, bool drawReflection, bool d smv.Submit(GL_TRIANGLES, false); } - // void CGLUnitDrawer::DrawGhostedBuildings(int modelType) - if (gu->spectatingFullView) - return; + smv.Unbind(); - const auto& deadGhostBuildings = modelDrawerData->GetDeadGhostBuildings(gu->myAllyTeam, modelType); + // living and dead ghosted buildings + if (!gu->spectatingFullView) + DrawGhostedBuildings(modelType); +} - const auto oldMM = modelDrawerState->SetMatrixMode(ShaderMatrixModes::STATIC_MATMODE); - // deadGhostedBuildings - { - modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); - modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); //teamID doesn't matter here +void CUnitDrawerGL4::DrawGhostedBuildings(int modelType) const +{ + RECOIL_DETAILED_TRACY_ZONE; - int prevModelType = -1; - int prevTexType = -1; - for (const auto* dgb : deadGhostBuildings) { - if (!camera->InView(dgb->pos, dgb->GetModel()->GetDrawRadius())) - continue; + auto& smv = S3DModelVAO::GetInstance(); + smv.Bind(); + + // Ghost buildings are static (no animation, never move), so each gets a single world-transform + // slot in the transforms SSBO and is drawn batched through ARRAY_MATMODE - one multidraw per + // (color bucket x texture type) instead of one immediate draw per ghost. + const auto oldMM = modelDrawerState->SetMatrixMode(ShaderMatrixModes::ARRAY_MATMODE); - static CMatrix44f staticWorldMat; + struct GhostInstance { + const S3DModel* model; + uint32_t worldTransformOffset; + uint16_t paletteIndex; // color the ghost was last seen under (see LiveGhostBuilding / GhostSolidObject) + }; + // bind the texture once per group, accumulate, then one Submit (=one multidraw) per texture type. + // buckets are reused across frames (see clearBuckets) so a screen full of ghosts does not realloc + // its per-texture vectors every frame; empty buckets (a texture no longer on screen) are skipped. + const auto flushGhosts = [&](const std::map>& byTex) { + for (const auto& [texType, instances] : byTex) { + if (instances.empty()) + continue; + CModelDrawerHelper::BindModelTypeTexture(modelType, texType); + for (const auto& gi : instances) + smv.AddStaticInstance(gi.model, gi.worldTransformOffset, gi.paletteIndex); + smv.Submit(GL_TRIANGLES, false); + } + }; + // clear the mapped vectors (keeping their capacity) instead of clearing the map (which would free them) + const auto clearBuckets = [](std::map>& byTex) { + for (auto& [texType, instances] : byTex) + instances.clear(); + }; - staticWorldMat.LoadIdentity(); - staticWorldMat.Translate(dgb->pos); + // deadGhostedBuildings (single color state) + { + const auto& deadGhostBuildings = modelDrawerData->GetDeadGhostBuildings(gu->myAllyTeam, modelType); - staticWorldMat.RotateY(-dgb->facing * math::DEG_TO_RAD * 90.0f); + static std::map> byTex; + clearBuckets(byTex); + bool any = false; + for (const auto* dgb : deadGhostBuildings) { + const S3DModel* model = dgb->GetModel(); + if (!camera->InView(dgb->pos, model->GetDrawRadius())) + continue; + if (!dgb->worldTransformAlloc.Valid()) + continue; - if (prevModelType != modelType || prevTexType != dgb->GetModel()->textureType) { - prevModelType = modelType; prevTexType = dgb->GetModel()->textureType; - CModelDrawerHelper::BindModelTypeTexture(modelType, dgb->GetModel()->textureType); //inefficient rendering, but w/e - } + byTex[model->textureType].push_back({ model, static_cast(dgb->worldTransformAlloc.GetOffset()), dgb->paletteIndex }); + any = true; + } - modelDrawerState->SetStaticModelMatrix(staticWorldMat); - smv.SubmitImmediately(dgb->GetModel(), static_cast(dgb->team)); //need to submit immediately every model because of static per-model matrix + if (any) { + modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); //teamID is per-instance + flushGhosts(byTex); } } - // liveGhostedBuildings + // liveGhostedBuildings (two color states: normal and CONTRADAR) { const auto& liveGhostedBuildings = modelDrawerData->GetLiveGhostBuildings(gu->myAllyTeam, modelType); - int prevModelType = -1; - int prevTexType = -1; - for (const auto* lgb : liveGhostedBuildings) { - if (!camera->InView(lgb->pos, lgb->model->GetDrawRadius())) + static std::map> byTexNormal; + static std::map> byTexContradar; + clearBuckets(byTexNormal); + clearBuckets(byTexContradar); + bool anyNormal = false; + bool anyContradar = false; + + for (const auto& lgb : liveGhostedBuildings) { + const CUnit* u = lgb.unit; + if (!camera->InView(u->pos, u->model->GetDrawRadius())) continue; // check for decoy models - const UnitDef* decoyDef = lgb->unitDef->decoyDef; - const S3DModel* model = nullptr; - - if (decoyDef == nullptr) { - model = lgb->model; - } - else { - model = decoyDef->LoadModel(); - } + const UnitDef* decoyDef = u->unitDef->decoyDef; + const S3DModel* model = (decoyDef == nullptr) ? u->model : decoyDef->LoadModel(); // FIXME: needs a second pass if (model->type != modelType) continue; - static CMatrix44f staticWorldMat; - - staticWorldMat.LoadIdentity(); - staticWorldMat.Translate(lgb->pos); - - staticWorldMat.RotateY(-lgb->buildFacing * math::DEG_TO_RAD * 90.0f); - - const unsigned short losStatus = lgb->losStatus[gu->myAllyTeam]; - - // ghosted enemy units - if (losStatus & LOS_CONTRADAR) { - modelDrawerState->SetColorMultiplier(0.9f, 0.9f, 0.9f, IModelDrawerState::alphaValues.z); - modelDrawerState->SetTeamColor(lgb->team, IModelDrawerState::alphaValues.z); - } - else { - modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); - modelDrawerState->SetTeamColor(lgb->team, IModelDrawerState::alphaValues.y); - } + const size_t xfOffset = modelDrawerData->GetLiveGhostTransform(u); + if (xfOffset == TransformsMemStorage::INVALID_INDEX) + continue; - if (prevModelType != modelType || prevTexType != model->textureType) { - prevModelType = modelType; prevTexType = model->textureType; - CModelDrawerHelper::BindModelTypeTexture(modelType, model->textureType); //inefficient rendering, but w/e - } + const unsigned short losStatus = u->losStatus[gu->myAllyTeam]; + const bool contradar = (losStatus & LOS_CONTRADAR); + // bucket with the palette the unit was last seen under, not the live unit's current one + (contradar ? byTexContradar : byTexNormal)[model->textureType] + .push_back({ model, static_cast(xfOffset), lgb.paletteIndex }); + (contradar ? anyContradar : anyNormal) = true; + } - modelDrawerState->SetStaticModelMatrix(staticWorldMat); - smv.SubmitImmediately(model, static_cast(lgb->team)); //need to submit immediately every model because of static per-model matrix + if (anyNormal) { + modelDrawerState->SetColorMultiplier(0.6f, 0.6f, 0.6f, IModelDrawerState::alphaValues.y); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.y); + flushGhosts(byTexNormal); + } + if (anyContradar) { + modelDrawerState->SetColorMultiplier(0.9f, 0.9f, 0.9f, IModelDrawerState::alphaValues.z); + modelDrawerState->SetTeamColor(0, IModelDrawerState::alphaValues.z); + flushGhosts(byTexContradar); } } diff --git a/rts/Rendering/Units/UnitDrawer.h b/rts/Rendering/Units/UnitDrawer.h index d6c6935afc6..97106632f76 100644 --- a/rts/Rendering/Units/UnitDrawer.h +++ b/rts/Rendering/Units/UnitDrawer.h @@ -167,7 +167,7 @@ class CUnitDrawerGLSL : public CUnitDrawerBase { void DrawOpaqueUnit(CUnit* unit, uint8_t thisPassMask) const; void DrawUnitShadow(CUnit* unit) const; - void DrawAlphaUnit(CUnit* unit, int modelType, uint8_t thisPassMask, bool drawGhostBuildingsPass) const; + void DrawAlphaUnit(CUnit* unit, uint8_t thisPassMask) const; void DrawOpaqueAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; void DrawAlphaAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; @@ -236,7 +236,7 @@ class CUnitDrawerGL4 final : public CUnitDrawerGLSL { void DrawOpaqueObjectsAux(int modelType) const override; void DrawOpaqueAIUnit(const CUnitDrawerData::TempDrawUnit& unit) const; - void DrawGhostedBuildings(int modelType) const override {} //implemented in-line + void DrawGhostedBuildings(int modelType) const override; void DrawUnitModelBeingBuiltShadow(const CUnit* unit, bool noLuaCall) const; void DrawUnitModelBeingBuiltOpaque(const CUnit* unit, bool noLuaCall) const; diff --git a/rts/Rendering/Units/UnitDrawerData.cpp b/rts/Rendering/Units/UnitDrawerData.cpp index 3f98205280a..dfd048a332a 100644 --- a/rts/Rendering/Units/UnitDrawerData.cpp +++ b/rts/Rendering/Units/UnitDrawerData.cpp @@ -29,9 +29,22 @@ #include "Map/ReadMap.h" #include "System/Misc/TracyDefs.h" +#include "System/Matrix44f.h" +#include "System/MathConstants.h" +#include "System/Transform.hpp" +#include "Rendering/Models/ModelsMemStorage.h" static FixedDynMemPoolT ghostMemPool; +// world transform of a (static) ghost building, matching the legacy staticModelMatrix construction +static Transform MakeGhostWorldTransform(const float3& pos, int facing) +{ + CMatrix44f m; + m.Translate(pos); + m.RotateY(-facing * math::HALFPI); + return Transform::FromMatrix(m); +} + /////////////////////////// CR_BIND_POOL(GhostSolidObject, ,ghostMemPool.allocMem, ghostMemPool.freeMem) @@ -48,13 +61,22 @@ CR_REG_METADATA(GhostSolidObject, ( CR_MEMBER(facing), CR_MEMBER(team), + CR_MEMBER(paletteIndex), CR_IGNORED(currentIconIndex), + CR_IGNORED(worldTransformAlloc), CR_IGNORED(model), CR_POSTLOAD(PostLoad) )) +CR_BIND(CUnitDrawerData::LiveGhostBuilding, ) +CR_REG_METADATA(CUnitDrawerData::LiveGhostBuilding, ( + CR_MEMBER(unit), + CR_MEMBER(paletteIndex), + CR_MEMBER(team) +)) + CR_BIND(CUnitDrawerData::TempDrawUnit, ) CR_REG_METADATA(CUnitDrawerData::TempDrawUnit, ( CR_MEMBER(unitDefId), @@ -88,6 +110,15 @@ void GhostSolidObject::PostLoad() RECOIL_DETAILED_TRACY_ZONE; model = nullptr; GetModel(); + + // the GPU world transform slot is render-only state; re-create it from the saved pos/facing + InitWorldTransform(); +} + +void GhostSolidObject::InitWorldTransform() +{ + worldTransformAlloc = ScopedTransformMemAlloc(1); + worldTransformAlloc.UpdateForced(0, MakeGhostWorldTransform(pos, facing)); } const S3DModel* GhostSolidObject::GetModel() const @@ -157,6 +188,7 @@ CUnitDrawerData::~CUnitDrawerData() if (tmpGso->DecRef()) continue; + // worldTransformAlloc frees its slot in ~GhostSolidObject (ghostMemPool.free below) // might be the gbOwner of a decal; groundDecals is deleted after us groundDecals->GhostDestroyed(tmpGso); ghostMemPool.free(tmpGso); @@ -165,6 +197,7 @@ CUnitDrawerData::~CUnitDrawerData() lgb.clear(); } } + liveGhostTransforms.clear(); // each entry's ScopedTransformMemAlloc frees its slot on erase assert(ghostMemPool.allocs() == 0); ghostMemPool.clear(); @@ -216,6 +249,8 @@ void CUnitDrawerData::Update() updateBody(unit); } + UpdateLiveGhostTransforms(); + if ((useDistToGroundForIcons = (camHandler->GetCurrentController()).GetUseDistToGroundForIcons())) { const float3& camPos = camera->GetPos(); // use the height at the current camera position @@ -617,6 +652,7 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost gso->facing = u->buildFacing; gso->dir = u->frontdir; gso->team = u->team; + gso->paletteIndex = u->paletteIndex; gso->radius = u->radius; gso->GetModel(); @@ -625,6 +661,8 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost gso->iconRadius = u->iconRadius; + gso->InitWorldTransform(); + groundDecals->GhostCreated(u, gso); } @@ -640,7 +678,8 @@ bool CUnitDrawerData::UpdateUnitGhosts(const CUnit* unit, const bool addNewGhost } - spring::VectorErase(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(u)], u); + spring::VectorEraseIf(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(u)], + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); } return addedOwnAllyTeam; } @@ -674,7 +713,8 @@ void CUnitDrawerData::UnitEnteredLos(const CUnit* unit, int allyTeam) CUnit* u = const_cast(unit); //cleanup if (unit->leavesGhost) - spring::VectorErase(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], u); + spring::VectorEraseIf(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); if (allyTeam != gu->myAllyTeam) return; @@ -687,8 +727,15 @@ void CUnitDrawerData::UnitLeftLos(const CUnit* unit, int allyTeam) RECOIL_DETAILED_TRACY_ZONE; CUnit* u = const_cast(unit); //cleanup - if (unit->leavesGhost) - spring::VectorInsertUnique(savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)], u, true); + if (unit->leavesGhost) { + // snapshot the color the unit is last seen under so a later team change (while out of LOS) + // does not recolor its ghost. keep the earliest snapshot if it re-fires without re-entering. + auto& lgbs = savedData.liveGhostBuildings[allyTeam][MDL_TYPE(unit)]; + const bool alreadyGhosted = std::any_of(lgbs.begin(), lgbs.end(), + [u](const LiveGhostBuilding& lgb) { return lgb.unit == u; }); + if (!alreadyGhosted) + lgbs.push_back({ u, u->paletteIndex, static_cast(u->team) }); + } if (allyTeam != gu->myAllyTeam) return; @@ -696,6 +743,37 @@ void CUnitDrawerData::UnitLeftLos(const CUnit* unit, int allyTeam) UpdateCurrentUnitIcon(unit); } +void CUnitDrawerData::UpdateLiveGhostTransforms() +{ + RECOIL_DETAILED_TRACY_ZONE; + // Maintain one world-transform slot per live ghost building drawn for the local allyTeam. + // Ghosts are static, so each slot is filled once on first sight; entries not seen this sweep + // (units that regained LOS, died, or belong to a different allyTeam now) are freed. + const int stamp = ++liveGhostSweepStamp; + + for (int modelType = MODELTYPE_3DO; modelType < MODELTYPE_CNT; modelType++) { + for (const auto& lgb : savedData.liveGhostBuildings[gu->myAllyTeam][modelType]) { + const CUnit* u = lgb.unit; + const auto it = liveGhostTransforms.find(u); + if (it == liveGhostTransforms.end()) { + ScopedTransformMemAlloc alloc(1); + alloc.UpdateForced(0, MakeGhostWorldTransform(u->pos, u->buildFacing)); + liveGhostTransforms.emplace(u, std::make_pair(std::move(alloc), stamp)); + } + else { + it->second.second = stamp; + } + } + } + + for (auto it = liveGhostTransforms.begin(); it != liveGhostTransforms.end(); ) { + if (it->second.second != stamp) + it = liveGhostTransforms.erase(it); // ScopedTransformMemAlloc frees the slot on erase + else + ++it; + } +} + void CUnitDrawerData::UnitLeavesGhostChanged(const CUnit* unit, const bool leaveDeadGhost) { if (unit->leavesGhost) { @@ -732,6 +810,7 @@ void CUnitDrawerData::PlayerChanged(int playerID) void CUnitDrawerData::RemoveDeadGhost(GhostSolidObject* gso, std::vector& dgb, int index) { if (!gso->DecRef()) { + // worldTransformAlloc frees its slot in ~GhostSolidObject (ghostMemPool.free) groundDecals->GhostDestroyed(gso); ghostMemPool.free(gso); } diff --git a/rts/Rendering/Units/UnitDrawerData.h b/rts/Rendering/Units/UnitDrawerData.h index 36630545555..d5ae046da63 100644 --- a/rts/Rendering/Units/UnitDrawerData.h +++ b/rts/Rendering/Units/UnitDrawerData.h @@ -23,6 +23,8 @@ class GhostSolidObject { bool DecRef() { return ((refCount--) > 1); } const S3DModel* GetModel() const; void PostLoad(); + // (re)allocate and fill the batched-draw world transform slot from pos/facing + void InitWorldTransform(); public: std::string modelName; @@ -34,8 +36,16 @@ class GhostSolidObject { int refCount; int facing; //FIXME replaced with dir-vector just legacy decal drawer uses this + + // color identity captured when the ghost was created; a ghost keeps the color it was last + // seen under. team drives the legacy (GLSL) team-color path, paletteIndex drives the GL4 + // per-instance palette (equal to team unless a custom Lua color palette was assigned). uint8_t team; + uint16_t paletteIndex; + size_t currentIconIndex; + + ScopedTransformMemAlloc worldTransformAlloc; private: mutable const S3DModel* model; }; @@ -86,6 +96,16 @@ class CUnitDrawerData : public CUnitDrawerDataBase { private: mutable const UnitDef* unitDef; }; + // a still-alive building that left an observer's LOS. The unit is drawn as a ghost using the + // color it was last seen under (snapshotted here), so it does not silently recolor if the live + // unit changes team while out of LOS. team feeds the legacy (GLSL) team-color path, paletteIndex + // feeds the GL4 per-instance palette (equal to team unless a custom Lua palette was assigned). + struct LiveGhostBuilding { + CR_DECLARE_STRUCT(LiveGhostBuilding) + CUnit* unit = nullptr; + uint16_t paletteIndex = 0; + uint8_t team = 0; + }; struct SavedData { CR_DECLARE_STRUCT(SavedData) @@ -97,7 +117,7 @@ class CUnitDrawerData : public CUnitDrawerDataBase { std::vector, MODELTYPE_CNT>> deadGhostBuildings; /// buildings that left LOS but are still alive - std::vector, MODELTYPE_CNT>> liveGhostBuildings; + std::vector, MODELTYPE_CNT>> liveGhostBuildings; }; public: CUnitDrawerData(bool& mtModelDrawer_); @@ -151,6 +171,12 @@ class CUnitDrawerData : public CUnitDrawerDataBase { return savedData.liveGhostBuildings[allyTeam][modelType]; } + // world transform slot (in transformsMemStorage) for a live ghost building; INVALID_INDEX if none + size_t GetLiveGhostTransform(const CUnit* unit) const { + const auto it = liveGhostTransforms.find(unit); + return (it != liveGhostTransforms.end()) ? it->second.first.GetOffset(false) : TransformsMemStorage::INVALID_INDEX; + } + auto* GetSavedData() { return &savedData; } const auto* GetSavedData() const { return &savedData; } protected: @@ -191,6 +217,13 @@ class CUnitDrawerData : public CUnitDrawerDataBase { S3DModel* GetUnitModel(const CUnit* unit) const; void RemoveDeadGhost(GhostSolidObject* gso, std::vector& dgb, int index); + // rebuilds the per-unit world transform slots for live ghost buildings of the local allyTeam. + // scan-based so it self-heals across savegame load, allyTeam/spectator changes and leavesGhost toggles. + void UpdateLiveGhostTransforms(); + // maps unit -> { RAII-owned world transform slot, last sweep stamp seen } + spring::unordered_map> liveGhostTransforms; + int liveGhostSweepStamp = 0; + // icons bool useDistToGroundForIcons; float sqCamDistToGroundForIcons; diff --git a/rts/System/MemPoolTypes.h b/rts/System/MemPoolTypes.h index 3ab106b64b9..34e0824d50a 100644 --- a/rts/System/MemPoolTypes.h +++ b/rts/System/MemPoolTypes.h @@ -13,7 +13,7 @@ #include #include -#include "smmalloc/smmalloc.h" +#include "System/recoil-smmalloc.h" #include "System/UnorderedMap.hpp" #include "System/ContainerUtil.h" diff --git a/rts/System/StringUtil.h b/rts/System/StringUtil.h index ea7ca4b62c6..73d2aaeef26 100644 --- a/rts/System/StringUtil.h +++ b/rts/System/StringUtil.h @@ -30,26 +30,6 @@ struct _UTIL_CONCAT(doOnce, __LINE__) { _UTIL_CONCAT(doOnce, __LINE__)() { code; } }; static _UTIL_CONCAT(doOnce, __LINE__) _UTIL_CONCAT(doOnceVar, __LINE__); -static char lcstr[32768]; -static char lcsub[32768]; -static inline const char* StrCaseStr(const char* str, const char* sub) { - const char* pos = nullptr; - - if (str == nullptr) - return nullptr; - if (sub == nullptr) - return nullptr; - - std::strncpy(lcstr, str, sizeof(lcstr) - 1); - std::strncpy(lcsub, sub, sizeof(lcsub) - 1); - std::transform(lcstr, lcstr + sizeof(lcstr), lcstr, (int (*)(int)) tolower); - std::transform(lcsub, lcsub + sizeof(lcsub), lcsub, (int (*)(int)) tolower); - - if ((pos = std::strstr(lcstr, lcsub)) == nullptr) - return nullptr; - - return (str + (pos - lcstr)); -} static inline void StringToLower(const char* in, char* out, size_t len) { diff --git a/rts/System/recoil-smmalloc.h b/rts/System/recoil-smmalloc.h new file mode 100644 index 00000000000..4ee7b9cb246 --- /dev/null +++ b/rts/System/recoil-smmalloc.h @@ -0,0 +1,15 @@ +/* This file is part of the Spring engine (GPL v2 or later), see LICENSE.html */ + +#pragma once + +// smmalloc.h leaks `#define INLINE inline` -- a very generic token that +// collides with unrelated code (e.g. simdjson's layout_mode::INLINE +// enumerator) whenever both end up in the same translation unit. Include +// smmalloc only through this wrapper so the macro is dropped immediately and +// never escapes into engine code. + +#include "smmalloc/smmalloc.h" + +#ifdef INLINE +#undef INLINE +#endif