diff --git a/.github/workflows/msbuild.yml b/.github/workflows/msbuild.yml index 11ad799ad..d47a5885c 100644 --- a/.github/workflows/msbuild.yml +++ b/.github/workflows/msbuild.yml @@ -51,11 +51,6 @@ jobs: token: ${{ secrets.ACTIONSPAT }} submodules: recursive - - name: PowerShell Module Cache - uses: potatoqualitee/psmodulecache@v6.2.1 - with: - modules-to-cache: 7Zip4PowerShell - - name: Run PS commands shell: pwsh run: .\utils\windows_dev_prepare.ps1 diff --git a/.gitignore b/.gitignore index b90c04b59..e52c9e912 100644 --- a/.gitignore +++ b/.gitignore @@ -23,10 +23,8 @@ android-project/.idea build/* samples/*/build -src_dependency/wxWidgets/* src_dependency/openal-soft/* src_dependency/SDL2/* -!src_dependency/wxWidgets/premake5.lua !src_dependency/openal-soft/premake5.lua !src_dependency/SDL2/premake5.lua src_dependency/slang/* diff --git a/README.md b/README.md index f24a70043..858bbaf39 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ While it demonstrates some best practices used in AA and AAA game development, s - Build All #### Linux -- Install packages: `build-essential SDL2-devel openal-soft-devel libXxf86vm-devel wxBase3-devel wxGTK3-devel` +- Install packages: `build-essential SDL2-devel openal-soft-devel libXxf86vm-devel` - Run `utils/linux_dev_prepare.sh` - Run `utils/premake5 --file=samples\GPUDriven\premake5.lua gmake2 && utils/premake5 --file=samples\GPUDriven\premake5.lua vscode` in repo root - Open VS Code and open `samples\GPUDriven\build` folder, install recommended C++ extensions diff --git a/core/materialsystem1/MaterialSystem.cpp b/core/materialsystem1/MaterialSystem.cpp index 421389a3e..281da8a10 100644 --- a/core/materialsystem1/MaterialSystem.cpp +++ b/core/materialsystem1/MaterialSystem.cpp @@ -716,6 +716,7 @@ void CMaterialSystem::ReleaseUnusedMaterials() { CScopedMutex m(s_matSystemMutex); + int flushedCount = 0; for (auto it = m_loadedMaterials.begin(); !it.atEnd(); ++it) { CMaterial* material = (CMaterial*)*it; @@ -724,12 +725,36 @@ void CMaterialSystem::ReleaseUnusedMaterials() if(!CString::CompareCaseIns(material->GetName(), "Default")) continue; - const int framesDiff = (material->m_frameBound - m_frame); + if (material->GetState() != MATERIAL_LOAD_OK) + continue; + + const uint framesDiff = m_frame - material->m_frameBound; + if(framesDiff < (uint)m_config.materialFlushThresh) + continue; - // Flush materials - if(framesDiff >= m_config.materialFlushThresh - 1) - material->Cleanup(false, true); + material->Flush(); + ++flushedCount; } + + // drop pipeline caches (pipelines, layouts, shared bind groups) of shader classes without materials. + // Pending pipeline jobs only resolve their futures, so dropping them is safe. + int droppedCaches = 0; + { + CScopedMutex m(s_matSystemPipelineMutex); + + Array unusedCaches(PP_SL); + for (auto it = m_renderPipelineCache.begin(); !it.atEnd(); ++it) + { + if (it.value().users <= 0) + unusedCaches.append(it.key()); + } + + for (int shaderNameHash : unusedCaches) + m_renderPipelineCache.remove(shaderNameHash); + droppedCaches = unusedCaches.numElem(); + } + + DevMsg(DEVMSG_MATSYSTEM, "ReleaseUnusedMaterials: %d materials flushed, %d pipeline caches dropped\n", flushedCount, droppedCaches); } // Reloads materials @@ -740,9 +765,6 @@ void CMaterialSystem::ReloadAllMaterials() MsgInfo("Reloading all materials and shaders...\n"); Array loadingList(PP_SL); - // drop pipeline cache - m_renderPipelineCache.clear(true); - const int oldFreq = m_shaderAPI->GetProgressiveTextureFrequency(); m_shaderAPI->SetProgressiveTextureFrequency(0); @@ -752,12 +774,24 @@ void CMaterialSystem::ReloadAllMaterials() m_shaderAPI->ReloadShaderPackage(it.key()); } + // free all shaders first, they release pipeline caches + for (auto it = m_loadedMaterials.begin(); !it.atEnd(); ++it) + { + CMaterial* material = static_cast(*it); + material->Cleanup(material->m_loadFromDisk, true); + } + + // drop pipeline cache + { + CScopedMutex m(s_matSystemPipelineMutex); + m_renderPipelineCache.clear(true); + } + for (auto it = m_loadedMaterials.begin(); !it.atEnd(); ++it) { CMaterial* material = static_cast(*it); const bool loadedFromDisk = material->m_loadFromDisk; - material->Cleanup(loadedFromDisk, true); // don't reload materials which are not from disk if(!loadedFromDisk) @@ -805,26 +839,33 @@ void CMaterialSystem::ReloadShader(const char* name) return; } - // clear pipeline cache first - { - CScopedMutex m(s_matSystemPipelineMutex); - m_renderPipelineCache.remove(shaderNameHash); - } - Array loadingList(PP_SL); + Array reloadList(PP_SL); const int oldFreq = m_shaderAPI->GetProgressiveTextureFrequency(); m_shaderAPI->SetProgressiveTextureFrequency(0); - // reload materials which have that shader + // free shaders of materials which have that shader, they release the pipeline cache. + // Materials without shader (not loaded or flushed) will load the new one on use. for (auto it = m_loadedMaterials.begin(); !it.atEnd(); ++it) { CMaterial* material = static_cast(*it); - if (CString::Compare(material->m_shader->GetName(), name) != 0) + if (!material->m_shader || CString::Compare(material->m_shader->GetName(), name) != 0) continue; + material->Cleanup(material->m_loadFromDisk, true); + reloadList.append(material); + } + + // clear pipeline cache + { + CScopedMutex m(s_matSystemPipelineMutex); + m_renderPipelineCache.remove(shaderNameHash); + } + + for (CMaterial* material : reloadList) + { const bool loadedFromDisk = material->m_loadFromDisk; - material->Cleanup(loadedFromDisk, true); // don't reload materials which are not from disk if (!loadedFromDisk) @@ -934,10 +975,47 @@ void CMaterialSystem::RegisterShaderOverride(const char* shaderName, OVERRIDE_SH m_shaderOverrideList.insert(new_override, 0); } -MatSysShaderPipelineCache& CMaterialSystem::GetRenderPipelineCache(int shaderNameHash) +MatSysShaderPipelineCache& CMaterialSystem::AcquireRenderPipelineCache(int shaderNameHash) +{ + CScopedMutex m(s_matSystemPipelineMutex); + MatSysShaderPipelineCache& cache = m_renderPipelineCache[shaderNameHash]; + ++cache.users; + return cache; +} + +void CMaterialSystem::ReleaseRenderPipelineCache(int shaderNameHash) +{ + CScopedMutex m(s_matSystemPipelineMutex); + auto it = m_renderPipelineCache.find(shaderNameHash); + if (it.atEnd()) + return; + + MatSysShaderPipelineCache& cache = *it; + ASSERT_MSG(cache.users > 0, "Pipeline cache has no users"); + if (cache.users > 0) + --cache.users; +} + +void CMaterialSystem::ReleaseSharedBindGroups() { CScopedMutex m(s_matSystemPipelineMutex); - return m_renderPipelineCache[shaderNameHash]; + for (auto it = m_renderPipelineCache.begin(); !it.atEnd(); ++it) + { + MatSysShaderPipelineCache& cache = *it; + Threading::CScopedWriteLocker lock(cache.rwLock); + for (MatSysShaderPipelineCache::SharedBindGroup& shared : cache.sharedBindGroups) + shared = MatSysShaderPipelineCache::SharedBindGroup{}; + } +} + +uint CMaterialSystem::GenerateRenderPassVersion() +{ + uint version = Atomic::Increment(m_renderPassVersion); + + // COM_UINT_MAX means 'any version' for shared bind groups + if (version == COM_UINT_MAX) + version = Atomic::Increment(m_renderPassVersion); + return version; } const MatSysShaderFactory* CMaterialSystem::GetShaderFactory(const char* szShaderName, int instanceFormatId) @@ -979,6 +1057,9 @@ void CMaterialSystem::QueueLoading(const IMaterialPtr& pMaterial) if(Atomic::CompareExchange(material->m_state, MATERIAL_LOAD_NEED_LOAD, MATERIAL_LOAD_INQUEUE) != MATERIAL_LOAD_NEED_LOAD) return; + // loaded material is not flushed by ReleaseUnusedMaterials until it was unused for materialFlushThresh frames + material->m_frameBound = m_frame; + if( m_config.threadedloader ) { s_threadedMaterialLoader.AddMaterial(pMaterial); @@ -1628,7 +1709,12 @@ bool CMaterialSystem::SetupMaterialPipeline(IMaterial* material, ArrayCRefGetState(); if (matState != MATERIAL_LOAD_OK && matState != MATERIAL_LOAD_ERROR) + { + // material was flushed by ReleaseUnusedMaterials (or never queued), load it again + if (matState == MATERIAL_LOAD_NEED_LOAD) + QueueLoading(IMaterialPtr(material)); return false; + } IShaderAPI* renderAPI = m_shaderAPI; @@ -1837,6 +1923,7 @@ void CMaterialSystem::PrintLoadedMaterials() const Msg("*** Render pipeline list begin ***\n"); int totalCached = 0; int totalShaders = 0; + int totalBindGroups = 0; for (auto it = m_shaderFactoryList.begin(); !it.atEnd(); ++it) { const MatSysShaderFactory& shaderFactory = *it; @@ -1847,8 +1934,13 @@ void CMaterialSystem::PrintLoadedMaterials() const Msg("shader %s: %d\n", shaderFactory.shaderName, pipelineCache.pipelines.size()); totalCached += pipelineCache.pipelines.size(); + for (const MatSysShaderPipelineCache::SharedBindGroup& sharedBindGroup : pipelineCache.sharedBindGroups) + { + if(sharedBindGroup.bindGroup) + totalBindGroups += 1; + } ++totalShaders; } - Msg("Total render pipelines: %d from %d shaders\n", totalCached, totalShaders); + Msg("Total: %d pipelines, %d bind groups from %d shaders\n", totalCached, totalBindGroups, totalShaders); } diff --git a/core/materialsystem1/MaterialSystem.h b/core/materialsystem1/MaterialSystem.h index 9d321f336..7c56eb2d9 100644 --- a/core/materialsystem1/MaterialSystem.h +++ b/core/materialsystem1/MaterialSystem.h @@ -97,7 +97,10 @@ class CMaterialSystem : public IMaterialSystem bool IsMaterialExist(const char* szMaterialName) const; const MatSysShaderFactory* GetShaderFactory(const char* szShaderName, int instanceFormatId); - MatSysShaderPipelineCache& GetRenderPipelineCache(int shaderNameHash); + MatSysShaderPipelineCache& AcquireRenderPipelineCache(int shaderNameHash); + void ReleaseRenderPipelineCache(int shaderNameHash); + void ReleaseSharedBindGroups(); + uint GenerateRenderPassVersion(); void PreloadNewMaterials(); bool WaitAllMaterialsLoaded(int timeout = -1); @@ -203,6 +206,7 @@ class CMaterialSystem : public IMaterialSystem Array m_proxyFactoryList{ PP_SL }; Map m_renderPipelineCache{ PP_SL }; + uint m_renderPassVersion{ 0 }; Map m_loadedMaterials{ PP_SL }; // loaded material list Array m_dynamicMeshes{ PP_SL }; diff --git a/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.cpp b/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.cpp index 78e8b45a7..89c723a82 100644 --- a/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.cpp +++ b/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.cpp @@ -109,6 +109,7 @@ void CNVRHIRenderAPI::Shutdown() m_rhiCommandLists.clear(true); m_rhiFreeCommandLists.clear(true); m_rhiSamplers.clear(true); + m_rhiBindingLayouts.clear(true); m_rhiDevice = nullptr; } @@ -796,6 +797,54 @@ nvrhi::SamplerHandle CNVRHIRenderAPI::GetRHISampler(const SamplerStateParams& sa return rhiSampler; } +static bool nvrhiBindingLayoutDescEqual(const nvrhi::BindingLayoutDesc& a, const nvrhi::BindingLayoutDesc& b) +{ + return a.visibility == b.visibility + && a.registerSpace == b.registerSpace + && a.registerSpaceIsDescriptorSet == b.registerSpaceIsDescriptorSet + && a.bindings == b.bindings + && a.bindingOffsets.shaderResource == b.bindingOffsets.shaderResource + && a.bindingOffsets.sampler == b.bindingOffsets.sampler + && a.bindingOffsets.constantBuffer == b.bindingOffsets.constantBuffer + && a.bindingOffsets.unorderedAccess == b.bindingOffsets.unorderedAccess; +} + +nvrhi::BindingLayoutHandle CNVRHIRenderAPI::GetRHIBindingLayout(const nvrhi::BindingLayoutDesc& rhiLayoutDesc) +{ + size_t hash = 0; + nvrhi::hash_combine(hash, static_cast(rhiLayoutDesc.visibility)); + nvrhi::hash_combine(hash, rhiLayoutDesc.registerSpace); + nvrhi::hash_combine(hash, rhiLayoutDesc.registerSpaceIsDescriptorSet); + nvrhi::hash_combine(hash, rhiLayoutDesc.bindingOffsets.shaderResource); + nvrhi::hash_combine(hash, rhiLayoutDesc.bindingOffsets.sampler); + nvrhi::hash_combine(hash, rhiLayoutDesc.bindingOffsets.constantBuffer); + nvrhi::hash_combine(hash, rhiLayoutDesc.bindingOffsets.unorderedAccess); + for (const nvrhi::BindingLayoutItem& item : rhiLayoutDesc.bindings) + { + nvrhi::hash_combine(hash, item.slot); + nvrhi::hash_combine(hash, static_cast(item.type)); + nvrhi::hash_combine(hash, static_cast(item.size)); + } + const uint layoutId = static_cast(hash ^ (hash >> 32)); + + CScopedMutex m(m_rhiBindingLayoutsMutex); + auto it = m_rhiBindingLayouts.find(layoutId); + if (it) + { + if (nvrhiBindingLayoutDescEqual(*(*it)->getDesc(), rhiLayoutDesc)) + return *it; + + // hash collision, layout is not shared + return m_rhiDevice->createBindingLayout(rhiLayoutDesc); + } + + nvrhi::BindingLayoutHandle rhiLayout = m_rhiDevice->createBindingLayout(rhiLayoutDesc); + if (rhiLayout) + m_rhiBindingLayouts.insert(layoutId, rhiLayout); + + return rhiLayout; +} + IGPURenderPipelinePtr CNVRHIRenderAPI::CreateRenderPipeline(const RenderPipelineDesc& pipelineDesc, const IGPUBindingLayout* bindingLayout) const { PROF_EVENT("CreateRenderPipeline"); diff --git a/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.h b/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.h index 69267a488..f1c73e0e5 100644 --- a/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.h +++ b/core/materialsystem1/Renderers/NVRHI/NVRHIRenderAPI.h @@ -101,6 +101,7 @@ class CNVRHIRenderAPI : public ShaderAPI_Base nvrhi::DeviceHandle GetNVRHIDevice() const { return m_rhiDevice; } nvrhi::SamplerHandle GetRHISampler(const SamplerStateParams& samplerStateParams); + nvrhi::BindingLayoutHandle GetRHIBindingLayout(const nvrhi::BindingLayoutDesc& rhiLayoutDesc); int AcquireRHITransientTextureHeap(); void ReleaseRHITransientTextureHeap(int heapIdx); @@ -132,6 +133,10 @@ class CNVRHIRenderAPI : public ShaderAPI_Base Map m_rhiSamplers{ PP_SL }; + // pipelines with equal binding layouts share layout objects, so they can share binding sets too + Map m_rhiBindingLayouts{ PP_SL }; + CEqMutex m_rhiBindingLayoutsMutex; + Map m_shaderCache{ PP_SL }; nvrhi::DeviceHandle m_rhiDevice{ nullptr }; ENVRHIBackendType m_rhiBackendType; diff --git a/core/materialsystem1/Renderers/NVRHI/NVRHIStates.cpp b/core/materialsystem1/Renderers/NVRHI/NVRHIStates.cpp index 05975f4bc..9e1d2b2ee 100644 --- a/core/materialsystem1/Renderers/NVRHI/NVRHIStates.cpp +++ b/core/materialsystem1/Renderers/NVRHI/NVRHIStates.cpp @@ -368,11 +368,10 @@ void nvrhiCreateBindingLayouts(const ShaderInfo& shaderInfo, const CNVRHIBinding } } - nvrhi::IDevice* rhiDevice = CNVRHIRenderAPI::Instance.GetNVRHIDevice(); for (nvrhi::BindingLayoutDesc& rhiDesc : rhiBindingLayoutDescList) { rhiDesc.setVisibility(rhiShaderType); - rhiBindingLayouts.append(rhiDevice->createBindingLayout(rhiDesc)); + rhiBindingLayouts.append(CNVRHIRenderAPI::Instance.GetRHIBindingLayout(rhiDesc)); } } } @@ -392,12 +391,27 @@ void nvrhiFillBindingSets(const ShaderInfo& shaderInfo, ArrayCRef shaderMod if (!bindingSetIt) { const BindGroupDesc& bindGroupDesc = bindGroup->m_bindGroupDesc; + nvrhi::IBindingLayout* rhiBindingLayout = rhiBindingLayouts[bindGroupDesc.groupIdx]; // we need to create binding set for this shader using provided layout auto rhiBindingSetDesc = nvrhi::BindingSetDesc(); bindGroup->m_bindingLayout->FillBindingSetDescByLayoutMap(bindGroupDesc, shaderInfo, shaderModuleIdxs, rhiBindingSetDesc); - - nvrhi::BindingSetHandle rhiBindSet = nvrhiDevice->createBindingSet(rhiBindingSetDesc, rhiBindingLayouts[bindGroupDesc.groupIdx]); + + // pipelines with the same layout and bindings (e.g. instanced and non-instanced permutations) share the binding set + nvrhi::BindingSetHandle rhiBindSet; + for (auto it = bindGroup->m_rhiBindingSets.begin(); !it.atEnd(); ++it) + { + nvrhi::IBindingSet* rhiExistingSet = *it; + if (rhiExistingSet->getLayout() == rhiBindingLayout && *rhiExistingSet->getDesc() == rhiBindingSetDesc) + { + rhiBindSet = rhiExistingSet; + break; + } + } + + if (!rhiBindSet) + rhiBindSet = nvrhiDevice->createBindingSet(rhiBindingSetDesc, rhiBindingLayout); + if (!rhiBindSet) { ASSERT_FAIL("Failed to create binding set for shared bind group %s\n", bindGroupDesc.name.ToCString()); diff --git a/core/materialsystem1/material.cpp b/core/materialsystem1/material.cpp index f0bdd6c99..323a44713 100644 --- a/core/materialsystem1/material.cpp +++ b/core/materialsystem1/material.cpp @@ -377,6 +377,10 @@ MatVarProxyUnk CMaterial::FindMaterialVar(const char* pszVarName) const const ITexturePtr& CMaterial::GetBaseTexture(int stage) { + // shader is dropped when the material is flushed by ReleaseUnusedMaterials + if (!m_shader && m_state == MATERIAL_LOAD_NEED_LOAD) + DoLoadShaderAndTextures(); + if(m_shader != nullptr && !IsError()) { DoLoadShaderAndTextures(); @@ -450,6 +454,23 @@ void CMaterial::Cleanup(bool dropVars, bool dropShader) Atomic::Exchange(m_state, MATERIAL_LOAD_NEED_LOAD); } +void CMaterial::Flush() +{ + WaitForLoading(); + + if (m_shader) + { + m_shader->Unload(); + SAFE_DELETE(m_shader); + } + + // shader data stored by shaders + m_storage.clear(true); + m_needUpdateGPUProxies = true; + + Atomic::Exchange(m_state, MATERIAL_LOAD_NEED_LOAD); +} + void CMaterial::OnMatVarChanged(int varIdx, void* userData) { reinterpret_cast(userData)->OnVarUpdated(); diff --git a/core/materialsystem1/material.h b/core/materialsystem1/material.h index 45e718d81..6cc536fde 100644 --- a/core/materialsystem1/material.h +++ b/core/materialsystem1/material.h @@ -45,6 +45,9 @@ class CMaterial : public IMaterial void Cleanup(bool dropVars = true, bool dropShader = true); + // unloads shader and its GPU resources but keeps vars and proxies; material is loaded again on next use + void Flush(); + bool LoadShaderAndTextures(); void WaitForLoading() const; diff --git a/docs/architecture.md b/docs/architecture.md index d5dab5429..0c3894723 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -24,6 +24,7 @@ Related documents: | [animating_egf_overview.md](animating_egf_overview.md) | EGF animation (`shared/animating`): sequences, activities, blending, pose controllers, events, IK | | [rendering_matsystem_shaders_overview.md](rendering_matsystem_shaders_overview.md) | Material shaders: `CBaseShader`, pipelines, bind groups, `.def` files, overrides, `BaseUnlit` with GRIM | | [rendering_grim_overview.md](rendering_grim_overview.md) | GRIM: GPU-driven instance rendering, its shaders, `samples/GPUDriven` | +| [equi_overview.md](equi_overview.md) | EqUI (`shared/equi`): game UI controls, layout `.res` files, scaling, events, Lua bindings | | [sys_application_overview.md](sys_application_overview.md) | Application framework (`shared/sys`): start-up, game loop, app states, console, configs | | [input_overview.md](input_overview.md) | Input: key codes, command bindings, vector actions, touch zones, controllers | | [physics_overview.md](physics_overview.md) | Physics (`shared/physics`, eqPhysics): world, collision objects, rigid bodies, ray tests, constraints | @@ -86,7 +87,7 @@ Public interfaces are declared in `public/` and implemented in `core/` or `share | `shared/animating/` | Skeletal animation, bone setup, activities and events | | `shared/audio/` | Audio system on OpenAL, sound emitters/scripts/mixer; `source/` holds WAV/OGG sources | | `shared/physics/` | EqPhysics engine on top of Bullet collision — see [physics_overview.md](physics_overview.md) | -| `shared/equi/` | EqUI interface library: manager, controls, scheme | +| `shared/equi/` | EqUI interface library: manager, controls, scheme — see [equi_overview.md](equi_overview.md) | | `shared/imgui_backend/` | ImGui host (`g_imGuiHost`), SDL platform backend and material-system renderer backend | | `shared/scripting/` | ESL — Lua binding library | | `shared/movie/` | WebM movie player: AV1 video (dav1d), Vorbis audio, YUV to RGBA conversion on GPU (`MovieYUV` shader) | @@ -116,7 +117,6 @@ loaded at the top of `premake5.lua`: | `unitybuild` | `unitybuild`, `maxfilesinunity`, `excludefromunity` — MSVC unity build support | | `androidndk` | `androidndk` action for Android builds | | `vscode` | `vscode` action and `vscode_*` launch settings | -| `wxwidgets` | wxWidgets integration for GUI tools | ### usage / uses diff --git a/docs/building.md b/docs/building.md index 69e1a5e2f..444e2bcba 100644 --- a/docs/building.md +++ b/docs/building.md @@ -18,8 +18,6 @@ releases and works differently from them. - Windows SDK 10.0.26100 or newer (required by Tracy). The SDK version can be set with the `WINSDK_VER` environment variable (for example `10.0.26100.0`); without it the latest installed SDK is used. -- PowerShell module `7Zip4PowerShell`, used to unpack dependency archives. Install it once with - `utils/win_install_7zip4powershell.ps1`. ### Dependencies @@ -27,13 +25,11 @@ releases and works differently from them. powershell -ExecutionPolicy Bypass -File utils\windows_dev_prepare.ps1 ``` -The script downloads SDL2, OpenAL-soft, wxWidgets and Slang and unpacks them into `src_dependency/`. +The script downloads SDL2, OpenAL-soft and Slang and unpacks them into `src_dependency/`. Run it once after cloning and again after the dependency versions in the script change. -wxWidgets is not used by the engine projects, but is required by the game project that includes -the engine (`src_game`). -`utils/cross_windows_dev_prepare.sh` does the same from a Linux shell (needs `wget`, `unzip` and -`7z`), for example to prepare a Windows checkout from WSL. +`utils/cross_windows_dev_prepare.sh` does the same from a Linux shell (needs `wget` and `unzip`), for +example to prepare a Windows checkout from WSL. ### Generating projects diff --git a/docs/equi_overview.md b/docs/equi_overview.md new file mode 100644 index 000000000..3c7f87c91 --- /dev/null +++ b/docs/equi_overview.md @@ -0,0 +1,354 @@ +# EqUI overview — `shared/equi` + +EqUI is the engine's retained-mode game UI: menus, HUD, loading screens. Layouts are trees of +controls described in KeyValues `.res` files and changed at runtime from C++ or Lua. It is built +as `equiLib` and drawn with the material system and the font system. Debug and tool UI uses ImGui +instead (`g_imGuiHost`, see [sys_application_overview.md](sys_application_overview.md)). + +| File | Contents | +|---|---| +| `EqUI_Manager.h/.cpp` | `equi::Manager` (`CUIManager`) — root panel, panels, control factories, focus, rendering, input | +| `IEqUI_Control.h/.cpp` | `equi::IUIControl` — base control: KeyValues parsing, layout, fonts, children, events, rendering | +| `equi_defs.h` | Anchors (`UI_BORDER_*`), alignment (`UI_ALIGN_*`), scaling modes (`UI_SCALING_*`), event flags (`UI_EVENT_*`) | +| `EqUI_Panel.h/.cpp` | `Panel` (top-level panel or window), `Container`, `HudElement` | +| `EqUI_Label.h/.cpp` | `Label` — text | +| `EqUI_Button.h/.cpp` | `Button` — clickable button | +| `EqUI_Image.h/.cpp` | `Image` — material or atlas image | +| `EqUI_Mask.h/.cpp` | `Mask` — draws its children through a mask texture | +| `EqUI_ProgressBar.h/.cpp` | `ProgressBar` | +| `EqUI_Editor.h/.cpp` | Layout editor (ImGui): document, outliner, preview, KeyValues view | +| `EqUI_EditorProps.h/.cpp` | Layout editor property inspector | +| `shared/sys/scripting/sys_esl_equi.cpp` | Lua bindings (`equi`, `equi_cast`, `fonts`) | + +## Manager and panels + +`CGameHost` initializes the manager after the font cache (`equi::Manager->Init()`), renders it +every frame and passes mouse and keyboard events to it (see +[sys_application_overview.md](sys_application_overview.md)). + +- **Root panel:** `GetRootPanel()` is an invisible panel (`equi_root`) that covers the view frame. + Every top-level panel is its child. +- **Panels:** `AddPanel(panel)` adds a top-level panel to the root. Panel names must be unique + (`FindPanel(name)` is case-insensitive). `DestroyPanel(panel)` removes and deletes it. + `BringToTop(panel)` / `GetTopPanel()` change and return the drawing order. +- **View frame:** the host sets it to the window or the state viewport size each frame + (`SetViewFrame`). `GetScreenSize()` returns its size. +- **Focus:** `SetFocus(control)` / `GetFocus()` — the keyboard focus. Clicking a control focuses + it; clicking the root panel clears the focus. `GetMouseOver()` is the control under the cursor. +- **Default font:** `GetDefaultFont()` — `"default"` at size 22, used when no control in the + parent chain has a font. + +```cpp +equi::Panel* panel = equi::Manager->Create(); // factory by class name +panel->SetName("PauseMenu"); +equi::Manager->AddPanel(panel); + +KVSection uiKvs; +if (KV_LoadFromFile("resources/ui_pausemenu.res", SP_MOD, uiKvs)) + panel->InitFromKeyValues(uiKvs); + +// later +equi::Manager->DestroyPanel(panel); +``` + +### Control factories + +Controls are created by class name (case-insensitive) through registered factories: + +| Name | Class | Purpose | +|---|---|---| +| `panel` | `Panel` | Top-level panel or window, draws a filled rectangle (`color`) | +| `container` | `Container` | Group without drawing; can include children from a file | +| `HudElement` | `HudElement` | Group without drawing, used in HUD layouts | +| `label` | `Label` | Text | +| `button` | `Button` | Button with a label, raises `click` | +| `image` | `Image` | Material or atlas image | +| `mask` | `Mask` | Masked children | +| `progressBar` | `ProgressBar` | Horizontal fill bar | + +`CreateElement(name)` returns a new control; `Create()` creates by `T::Classname()` and casts. +To add a control type, derive from `IUIControl` (or another control), add `EQUI_CLASS(Class, Base)` +in the class body, implement `DrawSelf`, define the factory with +`DECLARE_EQUI_CONTROL(name, Class)` in its `.cpp` and register it with +`EQUI_REGISTER_CONTROL(name)` after `equi::Manager->Init()`. On non-MSVC compilers, declare the +factory with `EQUI_CONTROL_DECLARATION(name)` and register with `EQUI_REGISTER_CONTROL2(name)`. + +Type checks use class names: `control->As()` / `equi::DynamicCast(control)` +return `nullptr` when the class name differs (an exact match, not inheritance). + +## Layout files + +A layout file has one root section (`panel` for panels), with nested `child ""` +sections: + +``` +panel +{ + selfVisible 0; + position 0 0; + size 640 480; // layout units: children are scaled from this size + + child file "resources/ui_partial/menu_fonts.res"; // include children and fonts + + child image "MenuTitle" + { + path "ui/ui_menu_title_gradient"; + position -50 100; + size 300 30; + scaling uniform; + color 1.0 0.57 0.0 1; + + child label "text" + { + align left vcenter; + position 100 3; + size 200 20; + scaling uniform; + label "#MENU_TITLE"; // '#' = localization token + } + } + + child button "Resume" + { + label "Resume"; + position 10 64; + size 210 40; + command engine "echo Resume clicked"; + } +} +``` + +- `InitFromKeyValues(sec, keepElements)` parses the control and creates its children. With + `keepElements = false` existing children are deleted first. With `true`, a child with the same + name is updated in place (or replaced if its class differs), which is how HUD layouts are + layered over a base layout. +- `Panel::InitFromKeyValues` uses the nested `panel` section if there is one, otherwise the given + section. +- `child file ""` loads another file (path as is, `SP_MOD`) with + `CUIManager::LoadLayoutFile` and adds its fonts and children to this control (as with + `keepElements = true`). `SetLayoutFileLoader` replaces the file system for these loads (the + editor gives its unsaved copies). `Container` also accepts `file ""`, loaded from + `resources/` directly. + +### Common properties — `IUIControl::Parse` + +| Key | Meaning | +|---|---| +| `position x y`, `size w h` | Rectangle in layout units, relative to the parent and its alignment | +| `visible 0/1` | Visibility (`Show`, `Hide`); hidden controls are not drawn and not hit-tested | +| `selfVisible 0/1` | Draw the control itself; children are still drawn | +| `label "text"` | UTF-8 text, or `#TOKEN` for a localized string | +| `align ...` | Alignment inside the parent: `left`, `right`, `hcenter`, `top`, `bottom`, `vcenter`, `center` | +| `anchors ...` | Sides kept at the same distance when the parent is resized: `left`, `top`, `right`, `bottom`, `all` | +| `scaling mode` | How the parent's resize scales the control: `none`, `inherit`, `inherit_min`, `inherit_max`, `aspectw`, `aspecth` / `uniform`, `aspect_min`, `aspect_max` | +| `transform { rotate deg; translate x y; elementScale x y; }` | Extra transform around the control center (does not affect hit testing) | +| `clipChilds 0/1`, `clipTransform 0/1` | Clip children to this control's rectangle; apply the transform to the clip rectangle. Default is inherited from the parent | +| `command ` | Handler for the `command` event, see [Events](#events) | +| `font "Name" [size] [bold] [italic]` | Font of the control. A single name refers to an entry of `fonts` in this control or a parent | +| `fonts { Name "Font" size; ... }` | Named fonts for children (text properties can be set per entry) | +| `fontScale x y`, `textColor r g b a`, `textAlign ...`, `textCase upper/lower`, `textWeight`, `textMonospace`, `textShadowColor`, `textShadowOffset`, `textShadowWeight` | Text style | + +Control-specific keys: + +| Control | Keys | +|---|---| +| `panel` | `color`, `window 1` (window with title and close button from `resources/WindowControls.res`, hidden by default), `screenoverlay 1` (always covers the view frame) | +| `image` | `path "material"` with optional `uvLeftTop` / `uvRightBottom`, or `atlas "material" "entry"`; `color`, `flipX`, `flipY` | +| `mask` | `image "mask texture"`, `color` | +| `progressBar` | `value` (0..1), `color` | + +### Size and scaling + +Positions and sizes are in layout units. When the manager resizes the root panel to the view +frame, the difference between the current and the parsed size (`GetSizeDiff()`, +`GetSizeDiffPerc()`) is used by children: + +- **Scaling** multiplies position, size and font scale by the parent's size ratio + (`CalcScaling()`). `uniform` keeps the aspect ratio by using the vertical ratio, so a layout + made for 640×480 keeps its proportions on wide screens. +- **Anchors** move or stretch the control by the parent's size difference. +- **Alignment** places the control relative to the parent's client rectangle; with `right` / + `bottom`, `position` is an offset from that side. + +`GetClientRectangle()` returns the final rectangle in screen pixels. + +## Controls at runtime + +```cpp +equi::IUIControl* title = panel->Get("MenuTitle.text"); // path of child names +title->SetLabel("#MENU_PAUSE"); +title->SetTextColor(ColorRGBA(1, 1, 0, 1)); + +if (equi::ProgressBar* bar = panel->FindChildRecursive("Health")->As()) + bar->SetValue(0.75f); +``` + +- Children: `AddChild`, `RemoveChild(ctrl, destroy)`, `FindChild(name)` (direct children), + `FindChildRecursive(name)`, `Get("a.b.c")`, `ForEachChild(func)`, `ClearChilds(destroy)`, + `ClearAll` (children, fonts and event handlers). A control owns its children and deletes them. +- Children added later are drawn on top and tested first for mouse hits. +- Visibility and state: `Show`, `Hide`, `SetVisible`, `SetSelfVisible`, `Enable`. `IsVisible()` + and `IsEnabled()` include the parents. +- Geometry: `SetPosition`, `SetSize`, `SetRectangle`, `SetAnchors`, `SetAlignment`, `SetScaling`, + `SetTransform(translate, scale, rotate)`. +- Text: `SetLabel` (UTF-8 or `#TOKEN`), `SetLabelText` (wide string), `SetFontScale`, + `SetTextColor`, `SetTextAlignment`. Labels support font tags (`TEXT_STYLE_USE_TAGS`). +- `Image::SetMaterial`, `SetAtlasImage`, `SetColor`, `SetUVRegion` (in texture pixels); + `ProgressBar::SetValue`, `SetColor`; `Panel::SetColor`, `CenterOnScreen`. + +### Events + +Event handlers are deprecated: they are not used by the game and may be removed or reworked. Do +not build new UI logic on them. Event handlers are named callbacks stored in the control: + +- `AddEventHandler(name, callback)` returns an id (`StringId24(name)`); `RemoveEventHandler(id)`, + `RemoveEventHandlers(name)`. +- `RaiseEvent(name, userData)` / `RaiseEventUid(id, userData)` call the **first** handler with + that name and return its result (-1 if there is none). +- `Button` raises `click` when the left mouse button is released over it after being pressed on + it. +- The `command` key in a layout adds a `command` handler with these actions: + + | Command | Action | + |---|---| + | `command engine ""` | Executes a console command | + | `command showpanel ""` / `hidepanel ""` | Shows / hides a top-level panel | + | `command hideparent` | Hides the parent control | + + Nothing raises `command` at the moment: `Button` raises only `click`, and `RaiseEvent` is not + bound to Lua. A `command` key has an effect only if C++ code calls `RaiseEvent("command", ...)` + on the control. + +## Rendering + +`Render(colorTarget)` draws the root panel in one render pass on the given target (the backbuffer +or the state viewport). Each control: + +1. Pushes its transform (`transform` block, rotated and scaled around its center). +2. Sets the scissor rectangle (its client rectangle, clipped by parents with `clipChilds`). +3. Calls `DrawSelf(rect, recorder)` if `selfVisible`. The manager root panel is created with + `selfVisible` off; controls rendered without a parent (the game HUD layout) are drawn like any + other control. +4. Renders children, earliest first. + +`Mask` renders its children to a render target (`_ui__masked`) and draws it with the +`UIOverlayMask` shader and the mask texture. It must be `selfVisible`. + +## Input + +The host passes events in this order: ImGui → console → EqUI → app state (see +[sys_application_overview.md](sys_application_overview.md)). + +- EqUI handles input only while a panel with `window 1` is visible (`IsWindowsVisible()`). The + host also shows the cursor while such a panel is visible. +- Mouse: the manager hit-tests from the root panel, sends `UI_EVENT_MOUSE_IN` / `MOUSE_OUT` when + the control under the cursor changes, brings the clicked panel to the top and passes the event + to the control under the cursor (`ProcessMouseEvents`). A window panel can be dragged with the + left button. +- Keyboard: events go to the focused control (`ProcessKeyboardEvents`). +- An event is consumed when the control returns `true` (the default for all controls). + +## Lua + +`shared/sys/scripting/sys_esl_equi.cpp` binds EqUI to the engine Lua state (see +[scripting_esl_overview.md](scripting_esl_overview.md)): + +- `equi` — the manager: `GetRootPanel`, `CreateElement`, `AddPanel`, `DestroyPanel`, `FindPanel`, + `BringToTop`, `GetTopPanel`, `GetViewFrame`, `GetScreenSize`, `SetFocus`, `GetFocus`, + `GetMouseOver`, `IsWindowsVisible`, `GetDefaultFont`, plus `equi.FormatString(fmt, ...)` and + `equi.IsValid(control)`. +- Controls: `IUIControl` methods and properties (`name`, `label`, `visible`, `position`, `size`, + `anchors`, `alignment`, `scaling`, `rectangle`, ...), `Panel`, `Container`, `Image`, + `ProgressBar`. `control:SetLabel(fmt, ...)` formats the localized `fmt` with the extra + arguments. +- `equi_cast.panel(ctrl)`, `equi_cast.image(ctrl)`, `equi_cast.progressBar(ctrl)`, + `equi_cast.container(ctrl)` — typed casts, `nil` if the class differs. +- `fonts.LoadFontDescriptionFile(file)`; `UI_BORDER_*`, `UI_ALIGN_*`, `UI_SCALING_*`, + `TEXT_STYLE_*`, `TEXT_ALIGN_*` constants. + +```lua +local kvs = KeyValues.new() +if kvs:LoadFile("resources/ui_filmdirector.res", SP_MOD) then + layout:ClearAll(true) + layout:InitFromKeyValues(kvs:GetRoot(), false) +end + +local bar = equi_cast.progressBar(layout:FindChildRecursive("Health")) +if bar then + bar.value = 0.5 +end +``` + +## Console commands and debugging + +| Command / ConVar | Purpose | +|---|---| +| `ui_listpanels` | List top-level panels and their visibility | +| `ui_showpanel `, `ui_hidepanel ` | Show / hide a panel | +| `equi_debug ` | Draw control rectangles and names at the given tree depth (`-1` for all) | +| `equi_debugMaskClear 1` | Clear mask render targets to red | + +## Layout editor + +`EqUI_Editor.cpp` is an ImGui editor of layout files, built in all configurations except `Retail` +and `Profile` (`EQUI_EDITOR`). The host registers it as the `ENGINE/UI/EQUI EDITOR` debug menu +after `equi::Manager->Init()` and calls `equiEditorShutdown()` before `equi::Manager->Shutdown()`, +so it can be opened in any sysLib application, including the game, where game controls are +registered. `equiEditorOpenFile(path)` opens a file from code. + +- **Document:** the layout is edited as a KeyValues document loaded from `SP_MOD` (File → Open + lists `.res` files under `resources/`). The preview controls are recreated from the document + after every change. Controls are matched with their `child` sections by the path of names from + the panel, so controls with duplicate names among siblings cannot be told apart. +- **Included files:** `child file` files are loaded into the editor when first needed and are + edited like the layout: the section of a control is searched in the same order as + `InitChildItems` (the last section with the name wins), including nested includes. The preview + is built with `SetLayoutFileLoader`, so it shows unsaved changes of included files. Save writes + the layout and every changed included file; note that included files are often shared by + several layouts. An included file can be reloaded alone (R in the inspector list, Reload in the + outliner menu of its controls, File → Reload included files for all), its unsaved changes are + discarded. The inspector lists `child file` entries of the control (edit path, remove, + add, show in the KeyValues tab). `Container` `file` includes are not supported. +- **Preview:** the panel is a child of the editor's own root panel, which is not added to the + manager: it is not drawn by the host and does not receive game input. The editor renders it to + its own target with the size chosen in the tab (window size or a fixed resolution) the same way + as `CUIManager::Render`. With Stretch panel (default) the panel is resized to the preview size, + as the game does with HUD layouts, so children are scaled from the layout size. With Root + element the layout is rendered without a parent and without its own background, as the game + renders the HUD layout; it is on by default for files without a `panel` section. A fixed + resolution is centered in the tab, has rulers in preview pixels and can be zoomed (10-400%, + Ctrl + mouse wheel over the preview); the target keeps the resolution, only its display is + scaled. Clicking a control + selects it; selection and bounds are drawn without control transforms. The selection shows the + `position` and `size` of the control in layout units. Picking tests children + outside of the parent rectangle, and controls that draw nothing (`Container`, `HudElement`, + `selfVisible 0`) are picked only when no drawn control is under the cursor. Right click opens + the layout popup of the control (alignment grid, anchors, position, size, scaling, transform; also the + Layout submenu of the outliner). Arrow keys move the selected control while the preview is + focused (Shift - by 10 units); with `right` / `bottom` alignment the position is inverted, as + it is an offset from that side. +- **KeyValues tab:** the layout or a loaded included file as text; edits are applied back with + Apply. +- **Outliner:** the control tree. Controls written in included files are blue with the file + name. Controls without a section (window controls) are grayed out and read-only. The context + menu adds children of any class registered in the manager (`GetControlClassNames`, engine and + game controls), adds `child file` entries picked from `.res` files, moves children (up, down, to the beginning or the end), + deletes them and toggles editor-only. The outliner lists children in the reverse order of their + sections (`AddChild` prepends): upper controls are drawn on top and are written later in the + file. Moving changes the order of `child` keys in the section that has the control, so a control + written in an included file is moved inside that file. A visible + control under the mouse in the outliner is highlighted in the preview. + The checkbox before a control hides it in the preview only; hidden controls are kept by path + in the editor, they are not written to the document and are reset when a file is opened. +- **Inspector:** known keys of the control class (see [Layout files](#layout-files)) with typed + editors; a checkbox writes or removes the key, flag options wrap to the next line when the + column is narrow. `font` has a list of local fonts: names from `fonts` sections of the + control, its parents and their included files. Other keys are edited as raw values written as + in `.res` files. `transform` has its own editor (T, R, S: `translate`, `rotate`, + `elementScale`); other nested sections (`fonts`) are edited in the KeyValues tab. +- **Editor-only controls:** child sections with `editorOnly 1;` are shown in the preview (for + example sample content for a list) and removed from the file on save. This includes + `child file "" { editorOnly 1; }` (Add child file... has an Editor only option, the + inspector has a checkbox per included file): controls of such a file and their children are + shown as editor-only. `InitChildItems` ignores the keys of `child file` sections. +- There is no undo yet; File → Reload discards unsaved changes. diff --git a/docs/keyvalues.md b/docs/keyvalues.md index 97377c4c0..79ec0a0b1 100644 --- a/docs/keyvalues.md +++ b/docs/keyvalues.md @@ -48,8 +48,9 @@ SourceControl p4 // key with a value and a nested section } ``` -- A key is followed by values separated by whitespace; the entry ends with `;` or with a `{ ... }` - section. +- A key is followed by values separated by whitespace. A key without a section must end with `;` + (see the examples above): a newline does not terminate an entry, so without the `;` the following + tokens are read as more values of the same key. A `{ ... }` section also ends the value list. - Values may be unquoted (no spaces or special characters) or quoted; quoted strings support escapes such as `\n`, `\t`, `\"`. - `//` and `/* ... */` comments. diff --git a/docs/public_rendering_overview.md b/docs/public_rendering_overview.md index 8fc5a9ee7..2db6bebf9 100644 --- a/docs/public_rendering_overview.md +++ b/docs/public_rendering_overview.md @@ -12,6 +12,19 @@ render passes, pipelines) will be described in separate documents. | `eqNVRHI`, `eqWGPURHI`, `eqNullRHI` | shared libraries | RHI backends (D3D12/Vulkan, WebGPU, null) implementing `IShaderAPI`; selected by the `MaterialSystem.Renderer` list in `E2.CONFIG` | | `BaseShader` | static library | `CBaseShader` base class for writing material shaders (`public/materialsystem1/BaseShader.*`) | +`eqNVRHI` creates NVRHI binding sets for bind groups on demand; a bind group with a binding layout +looks up its binding set by pipeline id (`nvrhiFillBindingSets` in `NVRHIStates.cpp`). Binding +layouts are cached by their description (`CNVRHIRenderAPI::GetRHIBindingLayout`), so pipelines with +equal layouts get the same layout objects, and a bind group reuses its binding set for every +pipeline id whose layout and resulting bindings are equal (for example, instanced and non-instanced +permutations of one shader). +On D3D12 every binding set needs a sampler descriptor table, and the shader-visible sampler heap is +limited to 2048 descriptors (`D3D12_MAX_SHADER_VISIBLE_SAMPLER_HEAP_SIZE`). The engine's NVRHI fork +(`src_dependency/nvrhi/NVRHI`, branch `insbyte-changes`) therefore shares sampler tables between +binding sets that have the same sampler descriptors (`DeviceResources::acquireSamplerTable`), so heap +usage depends on the number of distinct sampler combinations, not on the number of materials and +pipeline permutations. + Public headers: | Header | Contents | @@ -39,6 +52,13 @@ Main responsibilities: - **Materials:** `GetMaterial(name)` (loads or returns cached), `CreateMaterial(name, params)` (from a `KVSection`), `IsMaterialExist`, `GetDefaultMaterial`, `QueueLoading`, `PreloadNewMaterials`, `WaitAllMaterialsLoaded`, `ReloadAllMaterials`, `ReleaseUnusedMaterials`. +- **Unused material release:** `ReleaseUnusedMaterials` flushes loaded materials not drawn for + `MaterialsInitSettings::materialFlushThresh` frames (`CMaterial::Flush`: the shader and its + textures are freed, vars and proxies are kept; a newly queued material counts as drawn) and drops + the pipeline caches of shader classes no shader object uses. A flushed material is queued for + loading again by its next `SetupMaterialPipeline` (that draw is skipped) or `GetBaseTexture`. + Call it only at a point where no render job uses materials; it is also the `mat_releaseUnused` + console command. - **Shaders:** `LoadShaderLibrary`, `RegisterShader`, `RegisterShaderOverride`, `ReloadShader`. - **Proxies:** `RegisterProxy(factory, name)`, `CreateProxyByName`, `SetProxyDeltaTime`. - **Global material variables:** `GetGlobalMaterialVarByName(name, default)` — variables shared by diff --git a/docs/rendering_matsystem_shaders_overview.md b/docs/rendering_matsystem_shaders_overview.md index c7a6e8fad..f998cc9c9 100644 --- a/docs/rendering_matsystem_shaders_overview.md +++ b/docs/rendering_matsystem_shaders_overview.md @@ -119,7 +119,10 @@ void BuildPipelineShaderQuery(Array& shaderQuery) const override streams, the material flags (cull, depth test / write, decal, Z-only, blend mode), the topology, the presence of an instance provider, the render target formats and MSAA. The pipeline is looked up in the shader object, then in the material-system-wide cache for this shader name -(`GetRenderPipelineCache`), and created otherwise: +(`AcquireRenderPipelineCache`), and created otherwise. Each shader object holds one user of the +cache from the first lookup until `Unload` (`ReleaseRenderPipelineCache`); +`ReleaseUnusedMaterials` drops caches without users, `ReloadAllMaterials` / `ReloadShader` drop +them after freeing the shaders. - `FillRenderPipelineDesc` builds the description: shader name and query, vertex layouts from the mesh instance format (only the streams in `usedLayoutBits`; an instance stream must be bound), @@ -131,6 +134,10 @@ in the shader object, then in the material-system-wide cache for this shader nam `false`) until the pipeline is ready. `RenderPassContext::waitForPipelines` or device validation create them synchronously. `r_showPipelineCacheMisses` logs pipelines created during rendering; `r_pipelineCacheDisable` disables the shared cache. +- The job owns its inputs (description, binding layout) and only resolves a future. The shader + object and the cache entry keep that future, and `EnsureRenderPipeline` polls it on the next + lookup. Nothing points back into the shader or material, so they may be freed (level unload, + `ReleaseUnusedMaterials`, material reload) while pipelines are still being created. ## Bind groups @@ -147,7 +154,13 @@ in the shader object, then in the material-system-wide cache for this shader nam constant groups). - `CreateBindGroup` — a new group each call (render pass / transient data). - `CreateSharedBindGroup` / `GetSharedBindGroup` — one group shared by all materials of the shader - class, with a version number. + class, with a version number. It is stored by the material system next to the pipeline cache of + the shader class (`MatSysShaderPipelineCache::sharedBindGroups`), not by materials, so it outlives + them. `GetSharedBindGroup` returns nothing when the version differs (`COM_UINT_MAX` matches any). + - Take versions from `IMaterialSystem::GenerateRenderPassVersion`: they are unique for the + process, so a group made for older render pass data never matches new data. + - Call `IMaterialSystem::ReleaseSharedBindGroups` before freeing resources the groups reference + (render targets, environment maps), e.g. on level unload. - `GetEmptyBindGroup` — for unused groups. `setupParams.uniformBuffers` holds buffers passed by the caller with a signature diff --git a/docs/sys_application_overview.md b/docs/sys_application_overview.md index edb680a9c..fe9be39bf 100644 --- a/docs/sys_application_overview.md +++ b/docs/sys_application_overview.md @@ -148,18 +148,35 @@ frame the loop sleeps `sys_sleep` ms (default 1). 7. `EndScene`: `g_matSystem->EndFrame()`. 8. Pending fullscreen toggle, `g_onEndFrame`. +The ImGui renderer backend (`imgui_impl_matsystem.cpp`) draws through material system dynamic meshes, +which hold up to 32767 vertices and indices. It sets `ImGuiBackendFlags_RendererHasVtxOffset`, so +ImGui can put more than 65535 vertices into one draw list with 16-bit indices, and splits draw +commands by triangles into chunks that are packed into dynamic meshes. User callbacks draw the +pending chunks first. + +ImGui draws everything with translucent blending, so a texture's alpha is treated as coverage. +Render targets whose alpha is not coverage (scene targets, previews) are drawn without blending: +`ImGui_ImplMatSystem_AddImageOpaque`, or `ImGui_ImplMatSystem_BeginOpaque` / +`ImGui_ImplMatSystem_EndOpaque` around draw list commands (`imgui_impl_matsystem.h`). These add draw +list callbacks, so each block flushes the pending chunks; group many images into one block. +Anti-aliased lines and text inside a block lose their smooth edges. Blending is reset to translucent +at the start of each draw list and on `ImDrawCallback_ResetRenderState`. + `GetFrameTime()` returns the current frame time; `GetWindowSize()`, `GetDefaultFont()`, `GetWindowHandle()`. ## Window and video modes -- ConVars: `vid_mode` (`"1280x720"`), `vid_fullscreen`, `vid_screen`, `vid_bpp`, +- ConVars: `vid_mode` (`"1280x720"`), `vid_fullscreen`, `vid_screen`, `vid_maximized`, `vid_bpp`, `r_antialiasing`. +- `vid_maximized` creates the window maximized; `SetWindowedMode()` keeps the size of a maximized + window instead of applying `vid_mode`. - `SetFullscreenMode(screenSize)`, `SetWindowedMode()`, `ToggleFullscreen()` (also Right Alt + Enter), `ApplyVideoMode()`, `GetVideoModes(list)`, `IsWindowed()`; commands `sys_set_fullscreen`, `sys_set_windowed`, `sys_vmode_list`. -- `SetWindowTitle(title)`; non-retail builds append the renderer, configuration, platform and build - number. +- `SetWindowTitle(title)` can be called at any time, the title is applied when the window and the + renderer exist (`InitAppStates` runs before them); non-retail builds append the renderer, + configuration, platform and build number. - Text input for on-screen keyboards: `RequestTextInput()`, `ReleaseTextInput()`, `IsTextInputShown()`. - Cursor: controlled by the current state (`GetMouseCursorProperties`); it is always shown and @@ -221,9 +238,11 @@ g_renderAPI->BeginRenderPass( .End()); ``` -- `DrawImGui(size)` resizes the color and depth targets (backbuffer formats, `ResizeRenderTarget`) +- `DrawImGui(size)` resizes the color and depth targets (backbuffer formats and samplers, so the + depth target can be sampled like the default depth buffer; `ResizeRenderTarget`) to the drawn size and puts an invisible button with the image in the current ImGui window. The - viewport is *visible* only in a frame where `DrawImGui` was called. + image is drawn without blending (`ImGui_ImplMatSystem_AddImageOpaque`): the alpha written by the + state is ignored. The viewport is *visible* only in a frame where `DrawImGui` was called. - Viewports are disabled in `Retail` and `Profile` builds (`HOST_VIEWPORT_ENABLED` in `sys_viewport.h`): `DrawImGui` does nothing and the viewport is never visible. - When the viewport is not visible (ImGui window closed, `Retail` or `Profile` build), diff --git a/framework/math/Utility.cpp b/framework/math/Utility.cpp index bb02b3436..8bae27d0f 100644 --- a/framework/math/Utility.cpp +++ b/framework/math/Utility.cpp @@ -506,7 +506,7 @@ static float ConvexOrient2D(const Vector2D& o, const Vector2D& a, const Vector2D void ConvexHull2D(Array& points, Array& hull) { const int n = points.numElem(); - if (n <= 3) + if (n < 3) return; int k = 0; diff --git a/premake5.lua b/premake5.lua index 3b8dd34f6..a547d53b2 100644 --- a/premake5.lua +++ b/premake5.lua @@ -5,7 +5,6 @@ require ".premake_modules/usage" require ".premake_modules/properties" require ".premake_modules/androidndk" require ".premake_modules/unitybuild" -require ".premake_modules/wxwidgets" require ".premake_modules/vscode" IS_ANDROID = (_ACTION == "androidndk") diff --git a/public/materialsystem1/BaseShader.cpp b/public/materialsystem1/BaseShader.cpp index beb4837f3..cad9dfa20 100644 --- a/public/materialsystem1/BaseShader.cpp +++ b/public/materialsystem1/BaseShader.cpp @@ -315,16 +315,16 @@ void CBaseShader::FillRenderPipelineDesc(const PipelineInputParams& inputParams, pipelineBuilder.End(); } +// Creates render pipeline on a worker thread. +// It owns everything it uses, so the material and shader that requested it may be freed meanwhile. class CBaseShader::PipelineCreatorJob : public IParallelJob { public: - using CachePipelineIterator = MatSysShaderPipelineCache::PipelineMap::Iterator; - - PipelineCreatorJob(IShaderAPI* renderAPI, const RenderPipelineDesc& pipelineDesc, PipelineInfo& pipelineInfo) + PipelineCreatorJob(IShaderAPI* renderAPI, const RenderPipelineDesc& pipelineDesc, const IGPUBindingLayoutPtr& layout) : IParallelJob(EqString::Format("PipelineCreator_%s", pipelineDesc.shaderName.ToCString())) , m_renderAPI(renderAPI) , m_pipelineDesc(pipelineDesc) - , m_pipelineInfo(pipelineInfo) + , m_layout(layout) {} void Execute() override; @@ -333,13 +333,13 @@ class CBaseShader::PipelineCreatorJob : public IParallelJob protected: RenderPipelineDesc m_pipelineDesc; - PipelineInfo& m_pipelineInfo; + IGPUBindingLayoutPtr m_layout; IShaderAPI* m_renderAPI; }; void CBaseShader::PipelineCreatorJob::Execute() { - IGPURenderPipelinePtr renderPipeline = m_renderAPI->CreateRenderPipeline(m_pipelineDesc, m_pipelineInfo.layout); + IGPURenderPipelinePtr renderPipeline = m_renderAPI->CreateRenderPipeline(m_pipelineDesc, m_layout); if (!renderPipeline) { m_pipelinePromise.SetError(-1, EqString::Format("%s cannot create render pipeline", GetName())); @@ -348,28 +348,86 @@ void CBaseShader::PipelineCreatorJob::Execute() m_pipelinePromise.SetResult(std::move(renderPipeline)); } +MatSysShaderPipelineCache& CBaseShader::GetPipelineCache() const +{ + if (MatSysShaderPipelineCache* cache = m_pipelineCache) + return *cache; + + // shader holds one user of the cache until Unload + Threading::CScopedWriteLocker lock(m_renderPipelinesLock); + if (!m_pipelineCache) + m_pipelineCache = &g_matSystem->AcquireRenderPipelineCache(GetNameHash()); + return *m_pipelineCache; +} + +IGPUBindGroupPtr CBaseShader::GetSharedBindGroup(EBindGroupId bindGroupId, uint version) const +{ + MatSysShaderPipelineCache& cache = GetPipelineCache(); + + Threading::CScopedReadLocker lock(cache.rwLock); + const MatSysShaderPipelineCache::SharedBindGroup& shared = cache.sharedBindGroups[bindGroupId]; + if (version != COM_UINT_MAX && version != shared.version) + return nullptr; + return shared.bindGroup; +} + +void CBaseShader::SetSharedBindGroup(EBindGroupId bindGroupId, IGPUBindGroup* bindGroup, uint version) const +{ + MatSysShaderPipelineCache& cache = GetPipelineCache(); + + Threading::CScopedWriteLocker lock(cache.rwLock); + MatSysShaderPipelineCache::SharedBindGroup& shared = cache.sharedBindGroups[bindGroupId]; + shared.bindGroup = IGPUBindGroupPtr(bindGroup); + shared.version = version; +} + +// Takes the result of asynchronous pipeline creation. +// Shader polls the future instead of being called back, so nothing references the shader from a job. +void CBaseShader::ResolvePendingPipeline(PipelineInfo& pipelineInfo) +{ + if (!pipelineInfo.pipelinePending) + return; + + Threading::CScopedWriteLocker lock(m_renderPipelinesLock); + if (!pipelineInfo.pipelinePending || !pipelineInfo.pipelineFuture.HasResult()) + return; + + if (!pipelineInfo.pipelineFuture.HasErrors()) + pipelineInfo.pipeline = pipelineInfo.pipelineFuture.GetResult(); + + pipelineInfo.pipelineFuture = nullptr; + pipelineInfo.pipelinePending = false; +} + const CBaseShader::PipelineInfo& CBaseShader::EnsureRenderPipeline(IShaderAPI* renderAPI, const PipelineInputParams& inputParams, uint pipelineId, bool onInit) { using namespace Threading; if (m_lastPipelineInfoHash == pipelineId && m_lastPipelineInfo) + { + ResolvePendingPipeline(*m_lastPipelineInfo); return *m_lastPipelineInfo; + } HOOK_TO_CVAR(r_showPipelineCacheMisses); HOOK_TO_CVAR(r_pipelineCacheDisable); PipelineInfoMap::Iterator it; - + { CScopedReadLocker lock(m_renderPipelinesLock); it = m_renderPipelines.find(pipelineId); - if (it) - { - m_lastPipelineInfo = &(*it); - m_lastPipelineInfoHash = pipelineId; - return *it; - } } + if (it) + { + PipelineInfo& foundPipelineInfo = *it; + ResolvePendingPipeline(foundPipelineInfo); + + m_lastPipelineInfo = &foundPipelineInfo; + m_lastPipelineInfoHash = pipelineId; + return foundPipelineInfo; + } + { CScopedWriteLocker lock(m_renderPipelinesLock); it = m_renderPipelines.insert(pipelineId); @@ -380,29 +438,33 @@ const CBaseShader::PipelineInfo& CBaseShader::EnsureRenderPipeline(IShaderAPI* r const bool usePipelineCache = r_pipelineCacheDisable->GetBool() == false; - MatSysShaderPipelineCache::PipelineMap::Iterator cacheIt; if (usePipelineCache) { - if (!m_pipelineCache) - m_pipelineCache = &g_matSystem->GetRenderPipelineCache(GetNameHash()); + MatSysShaderPipelineCache& pipelineCache = GetPipelineCache(); + MatSysShaderPipelineCache::Pipeline cachedPipeline; + bool foundInCache = false; { - CScopedReadLocker lock(m_pipelineCache->rwLock); - cacheIt = m_pipelineCache->pipelines.find(pipelineId); + CScopedReadLocker lock(pipelineCache.rwLock); + auto cacheIt = pipelineCache.pipelines.find(pipelineId); + if (cacheIt) + { + cachedPipeline = *cacheIt; + foundInCache = true; + } } - if (cacheIt) + + if (foundInCache) { - if ((*cacheIt).future) + newPipelineInfo.pipeline = cachedPipeline.pipeline; + newPipelineInfo.layout = cachedPipeline.layout; + if (!newPipelineInfo.pipeline && cachedPipeline.future) { - (*cacheIt).future.AddCallback([&cache = *cacheIt, &newPipelineInfo](const FutureResult& result) { - cache.pipeline = *result; - newPipelineInfo.pipeline = *result; - }); + newPipelineInfo.pipelineFuture = cachedPipeline.future; + newPipelineInfo.pipelinePending = true; + ResolvePendingPipeline(newPipelineInfo); } - newPipelineInfo.pipeline = (*cacheIt).pipeline; - newPipelineInfo.layout = (*cacheIt).layout; - m_lastPipelineInfo = &newPipelineInfo; m_lastPipelineInfoHash = pipelineId; return newPipelineInfo; @@ -412,7 +474,7 @@ const CBaseShader::PipelineInfo& CBaseShader::EnsureRenderPipeline(IShaderAPI* r // Create pipeline layout { // MatSystem shader by default defines three bind groups, which differ by the lifetime: - // + // // BINDGROUP_CONSTANT // - Bind group data is never going to be changed during the life time of the material // BINDGROUP_RENDERPASS @@ -461,36 +523,40 @@ const CBaseShader::PipelineInfo& CBaseShader::EnsureRenderPipeline(IShaderAPI* r if (usePipelineCache) { - CScopedWriteLocker lock(m_pipelineCache->rwLock); + MatSysShaderPipelineCache& pipelineCache = GetPipelineCache(); + CScopedWriteLocker lock(pipelineCache.rwLock); MatSysShaderPipelineCache::Pipeline cachePipeline; cachePipeline.layout = newPipelineInfo.layout; cachePipeline.pipeline = newPipelineInfo.pipeline; - m_pipelineCache->pipelines.insert(pipelineId, cachePipeline); + pipelineCache.pipelines.insert(pipelineId, cachePipeline); } } else { - PipelineCreatorJob* pipelineJob = PPNew PipelineCreatorJob(renderAPI, renderPipelineDesc, newPipelineInfo); + PipelineCreatorJob* pipelineJob = PPNew PipelineCreatorJob(renderAPI, renderPipelineDesc, newPipelineInfo.layout); pipelineJob->InitJob(); pipelineJob->DeleteOnFinish(); + Future pipelineFuture = pipelineJob->m_pipelinePromise.CreateFuture(); + newPipelineInfo.pipelineFuture = pipelineFuture; + newPipelineInfo.pipelinePending = true; + if (usePipelineCache) { + MatSysShaderPipelineCache& pipelineCache = GetPipelineCache(); + MatSysShaderPipelineCache::Pipeline cachePipeline; cachePipeline.layout = newPipelineInfo.layout; - cachePipeline.future = pipelineJob->m_pipelinePromise.CreateFuture(); - { - CScopedWriteLocker lock(m_pipelineCache->rwLock); - cacheIt = m_pipelineCache->pipelines.insert(pipelineId, cachePipeline); - } - cachePipeline.future.AddCallback([&cache = *cacheIt, &newPipelineInfo, onInit, inputParams, this](const FutureResult& result) { - if (result.IsError()) - { - MsgError("Material %s produced error:\n%s\n", this->m_material->GetName(), result.GetErrorMessage()); - return; - } + cachePipeline.future = pipelineFuture; + + CScopedWriteLocker lock(pipelineCache.rwLock); + pipelineCache.pipelines.insert(pipelineId, cachePipeline); + } + // The callback only reports the result. It captures values, not the shader or material, + // which may be freed before the job completes. + EqString pipelineName; #ifndef _RETAIL #define BYTE_TO_BINARY_PATTERN "%c%c%c%c%c%c%c%c" @@ -502,44 +568,35 @@ const CBaseShader::PipelineInfo& CBaseShader::EnsureRenderPipeline(IShaderAPI* r ((byte) & 0x08 ? '1' : '0'), \ ((byte) & 0x04 ? '1' : '0'), \ ((byte) & 0x02 ? '1' : '0'), \ -((byte) & 0x01 ? '1' : '0') - - if (*result && r_showPipelineCacheMisses->GetBool()) - { - if (onInit) - { - MsgInfo("Pre-create pipeline %s-%s_" BYTE_TO_BINARY_PATTERN "-PT%d-DF%d-CF%d\n", - GetName(), inputParams.meshInstFormat.name, - BYTE_TO_BINARY(inputParams.meshInstFormat.usedLayoutBits), - inputParams.primitiveTopology, inputParams.depthTargetFormat, inputParams.colorTargetFormat[0]); - } - else - { - MsgWarning("Render pipeline %s-%s_" BYTE_TO_BINARY_PATTERN "-PT%d-DF%d-CF%d was not pre-created\n", - GetName(), inputParams.meshInstFormat.name, - BYTE_TO_BINARY(inputParams.meshInstFormat.usedLayoutBits), - inputParams.primitiveTopology, inputParams.depthTargetFormat, inputParams.colorTargetFormat[0]); - } - } -#endif // !_RETAIL +((byte) & 0x01 ? '1' : '0') - cache.pipeline = *result; - newPipelineInfo.pipeline = *result; - cache.future = nullptr; - }); - } - else + if (r_showPipelineCacheMisses->GetBool()) { - Future pipelineFuture = pipelineJob->m_pipelinePromise.CreateFuture(); - pipelineFuture.AddCallback([&newPipelineInfo, onInit, inputParams, this](const FutureResult& result) { - if (result.IsError()) - { - MsgError("%s\n", result.GetErrorMessage()); - return; - } - newPipelineInfo.pipeline = *result; - }); + const int colorTargetFormat = inputParams.colorTargetFormat.numElem() ? inputParams.colorTargetFormat[0] : FORMAT_NONE; + pipelineName = EqString::Format("%s-%s_" BYTE_TO_BINARY_PATTERN "-PT%d-DF%d-CF%d", + GetName(), inputParams.meshInstFormat.name, + BYTE_TO_BINARY(inputParams.meshInstFormat.usedLayoutBits), + inputParams.primitiveTopology, inputParams.depthTargetFormat, colorTargetFormat); } +#undef BYTE_TO_BINARY_PATTERN +#undef BYTE_TO_BINARY +#endif // !_RETAIL + + pipelineFuture.AddCallback([materialName = EqString(m_material->GetName()), pipelineName, onInit](const FutureResult& result) { + if (result.IsError()) + { + MsgError("Material %s produced error:\n%s\n", materialName.ToCString(), result.GetErrorMessage()); + return; + } + + if (pipelineName.Length()) + { + if (onInit) + MsgInfo("Pre-create pipeline %s\n", pipelineName.ToCString()); + else + MsgWarning("Render pipeline %s was not pre-created\n", pipelineName.ToCString()); + } + }); g_parallelJobs->GetJobMng()->StartJob(pipelineJob); } @@ -684,7 +741,11 @@ void CBaseShader::InitShader(IShaderAPI* renderAPI) // Unload shaders, textures void CBaseShader::Unload() { + if (m_pipelineCache) + g_matSystem->ReleaseRenderPipelineCache(GetNameHash()); m_pipelineCache = nullptr; + m_lastPipelineInfo = nullptr; + m_lastPipelineInfoHash = 0; m_renderPipelines.clear(true); for (int i = 0; i < m_usedTextures.numElem(); ++i) { diff --git a/public/materialsystem1/BaseShader.h b/public/materialsystem1/BaseShader.h index febb7939a..ae33b8034 100644 --- a/public/materialsystem1/BaseShader.h +++ b/public/materialsystem1/BaseShader.h @@ -25,8 +25,6 @@ struct MatSysCamera; } \ class C##name##Shader : public CBaseShader { \ public: \ - inline static uint s_sharedBindGroupVersion[MAX_BINDGROUPS] = { COM_UINT_MAX, COM_UINT_MAX, COM_UINT_MAX, COM_UINT_MAX }; \ - inline static IGPUBindGroup* s_sharedBindGroup[MAX_BINDGROUPS]; \ ArrayCRef GetSupportedVertexLayoutIds() const override { \ return C##name##ShaderLocalNamespace::GetSupportedVertexLayoutIds(); \ } \ @@ -35,20 +33,6 @@ struct MatSysCamera; C##name##Shader(IMaterial* material) : CBaseShader(material) {} \ void Init(IShaderAPI* renderAPI) override { \ CBaseShader::Init(renderAPI); ShaderInitParams(renderAPI); \ - } \ - void Unload() override { \ - CBaseShader::Unload(); \ - for (int i = 0; i < BINDGROUP_COUNT; ++i) { if(s_sharedBindGroup[i] && s_sharedBindGroup[i]->Ref_Drop()) s_sharedBindGroup[i] = nullptr; } \ - } \ - IGPUBindGroupPtr GetSharedBindGroup(EBindGroupId bindGroupId, uint version) const override { \ - if(version != COM_UINT_MAX && version != s_sharedBindGroupVersion[bindGroupId]) return nullptr; \ - return IGPUBindGroupPtr(s_sharedBindGroup[bindGroupId]); \ - } \ - void SetSharedBindGroup(EBindGroupId bindGroupId, IGPUBindGroup* bindGroup, uint version) const override { \ - bindGroup->Ref_Grab(); \ - if(s_sharedBindGroup[bindGroupId]) s_sharedBindGroup[bindGroupId]->Ref_Drop(); \ - s_sharedBindGroup[bindGroupId] = bindGroup; \ - s_sharedBindGroupVersion[bindGroupId] = version; \ } #define END_SHADER_CLASS }; \ @@ -164,8 +148,10 @@ class CBaseShader : public IMatSystemShader mutable IGPUBindGroupPtr bindGroup[MAX_BINDGROUPS]; // pipeline bind groups IGPURenderPipelinePtr pipeline; IGPUBindingLayoutPtr layout; + Future pipelineFuture; // pending asynchronous pipeline, polled by EnsureRenderPipeline int vertexLayoutId{ 0 }; mutable uint instMngToken{ COM_UINT_MAX }; + bool pipelinePending{ false }; }; struct BindGroupSetupParams @@ -178,8 +164,10 @@ class CBaseShader : public IMatSystemShader uint pipelineId{ 0 }; }; - virtual IGPUBindGroupPtr GetSharedBindGroup(EBindGroupId bindGroupId, uint version) const = 0; - virtual void SetSharedBindGroup(EBindGroupId bindGroupId, IGPUBindGroup* bindGroup, uint version) const = 0; + // shared bind groups are stored per shader class by material system (see MatSysShaderPipelineCache) + IGPUBindGroupPtr GetSharedBindGroup(EBindGroupId bindGroupId, uint version) const; + void SetSharedBindGroup(EBindGroupId bindGroupId, IGPUBindGroup* bindGroup, uint version) const; + MatSysShaderPipelineCache& GetPipelineCache() const; virtual IGPUBindGroupPtr GetBindGroup(IShaderAPI* renderAPI, EBindGroupId bindGroupId, const BindGroupSetupParams& setupParams) const { return nullptr; } virtual void FillBindGroupLayout_Constant(const MeshInstanceFormatRef& meshInstFormat, BindGroupLayoutDesc& bindGroupLayout) const {} @@ -194,6 +182,7 @@ class CBaseShader : public IMatSystemShader virtual void BuildPipelineShaderQuery(Array& shaderQuery) const {} const PipelineInfo& EnsureRenderPipeline(IShaderAPI* renderAPI, const PipelineInputParams& inputParams, uint pipelineId, bool onInit); + void ResolvePendingPipeline(PipelineInfo& pipelineInfo); IGPUBindGroupPtr CreatePersistentBindGroup(BindGroupDesc& bindGroupDesc, EBindGroupId bindGroupId, IShaderAPI* renderAPI, const PipelineInfo& pipelineInfo) const; IGPUBindGroupPtr CreateSharedBindGroup(BindGroupDesc& bindGroupDesc, EBindGroupId bindGroupId, uint version, IShaderAPI* renderAPI, const PipelineInfo& pipelineInfo) const; @@ -224,7 +213,7 @@ class CBaseShader : public IMatSystemShader uint m_lastPipelineInfoHash{ 0 }; mutable PipelineInfoMap m_renderPipelines{ PP_SL }; - MatSysShaderPipelineCache* m_pipelineCache{ nullptr }; + mutable MatSysShaderPipelineCache* m_pipelineCache{ nullptr }; IMaterial* m_material{ nullptr }; diff --git a/public/materialsystem1/IMatSysShader.h b/public/materialsystem1/IMatSysShader.h index 25a41768c..e8e7ea1ce 100644 --- a/public/materialsystem1/IMatSysShader.h +++ b/public/materialsystem1/IMatSysShader.h @@ -55,18 +55,30 @@ enum EBindGroupId : int BINDGROUP_COUNT }; +// Per-shader-class data owned by the material system. +// Shader instances never keep references into it beyond a single call: pending pipelines are +// shared through futures and shaders poll them, so material can be freed at any time. struct MatSysShaderPipelineCache { struct Pipeline { - Future future; + Future future; // valid while pipeline is created asynchronously IGPUBindingLayoutPtr layout; IGPURenderPipelinePtr pipeline; }; using PipelineMap = Map; + // bind group shared between all materials of the shader class (e.g. render pass data) + struct SharedBindGroup + { + IGPUBindGroupPtr bindGroup; + uint version{ COM_UINT_MAX }; + }; + PipelineMap pipelines{ PP_SL }; + SharedBindGroup sharedBindGroups[BINDGROUP_COUNT]; Threading::CEqReadWriteLock rwLock; + int users{ 0 }; // shader objects using the cache; unused caches are dropped by ReleaseUnusedMaterials }; class IShaderMeshInstanceProvider diff --git a/public/materialsystem1/IMaterialSystem.h b/public/materialsystem1/IMaterialSystem.h index 15c170287..801e25714 100644 --- a/public/materialsystem1/IMaterialSystem.h +++ b/public/materialsystem1/IMaterialSystem.h @@ -133,7 +133,17 @@ class IMaterialSystem : public IEqCoreModule virtual bool IsMaterialExist(const char* szMaterialName) const = 0; virtual const MatSysShaderFactory* GetShaderFactory(const char* szShaderName, int instanceFormatId) = 0; - virtual MatSysShaderPipelineCache& GetRenderPipelineCache(int shaderNameHash) = 0; + // returns pipeline cache of the shader class and adds a user to it. The cache stays valid until + // ReleaseRenderPipelineCache is called by that user (or material reload drops it). + virtual MatSysShaderPipelineCache& AcquireRenderPipelineCache(int shaderNameHash) = 0; + virtual void ReleaseRenderPipelineCache(int shaderNameHash) = 0; + + // drops shader shared bind groups (and resources they reference) of all shader classes. + // Call it when render pass resources (render targets, env maps) are about to be freed. + virtual void ReleaseSharedBindGroups() = 0; + + // returns unique render pass data version so stale shared bind groups never match new data + virtual uint GenerateRenderPassVersion() = 0; virtual void QueueLoading(const IMaterialPtr& pMaterial) = 0; virtual void PreloadNewMaterials() = 0; @@ -142,6 +152,9 @@ class IMaterialSystem : public IEqCoreModule virtual void ReloadAllMaterials() = 0; virtual void ReloadShader(const char* name) = 0; + // unloads shaders and textures of materials not drawn for MaterialsInitSettings::materialFlushThresh frames + // (they are loaded again on next use) and drops pipeline caches of shader classes nobody uses. + // Call it only when no rendering job uses materials. virtual void ReleaseUnusedMaterials() = 0; virtual void FreeMaterial(IMaterial* pMaterial) = 0; diff --git a/shared/equi/EqUI_Editor.cpp b/shared/equi/EqUI_Editor.cpp new file mode 100644 index 000000000..7c20a0c8b --- /dev/null +++ b/shared/equi/EqUI_Editor.cpp @@ -0,0 +1,1910 @@ +////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) Inspiration Byte +// 2009-2025 +////////////////////////////////////////////////////////////////////////////////// +// Description: EqUI editor +////////////////////////////////////////////////////////////////////////////////// + +#include "core/core_common.h" +#include "core/IFileSystem.h" +#include "utils/KeyValues.h" +#include "materialsystem1/IMaterialSystem.h" + +#include "EqUI_Manager.h" +#include "EqUI_Panel.h" +#include "EqUI_EditorProps.h" +#include "EqUI_Editor.h" + +#ifdef EQUI_EDITOR +#include +#include "imgui_backend/imgui_impl_matsystem.h" + +namespace equi::Editor +{ + +static constexpr const char* s_resourcesDir = "resources"; +static constexpr const int s_maxIncludeDepth = 16; +static constexpr const int s_minZoomPercent = 10; +static constexpr const int s_maxZoomPercent = 400; + +struct PreviewSize +{ + const char* name; + IVector2D size; +}; + +static const PreviewSize s_previewSizes[] = { + { "Fit to window", IVector2D(0, 0) }, + { "640x480", IVector2D(640, 480) }, + { "800x600", IVector2D(800, 600) }, + { "1280x720", IVector2D(1280, 720) }, + { "1920x1080", IVector2D(1920, 1080) }, +}; + +static constexpr const ImU32 s_selectionColor = IM_COL32(255, 160, 0, 255); +static constexpr const ImU32 s_hoverColor = IM_COL32(255, 255, 255, 160); +static constexpr const ImU32 s_boundsColor = IM_COL32(0, 200, 255, 90); +static const ImVec4 s_editorOnlyTextColor(1.0f, 0.6f, 0.2f, 1.0f); +static const ImVec4 s_includeTextColor(0.55f, 0.75f, 1.0f, 1.0f); + +//------------------------------------------------------------------------------------------------- + +// layout file: the edited one or one included with 'child file' +struct LayoutDoc : RefCountedObject +{ + EqString filePath; + KVSection root; + bool modified{ false }; + bool valid{ true }; // include file that failed to load is kept to not load it again +}; +using LayoutDocPtr = CRefPtr; + +// in outliner order, it is reversed order of the sections (IUIControl::AddChild prepends children) +enum EMoveChild +{ + MOVE_CHILD_UP, + MOVE_CHILD_DOWN, + MOVE_CHILD_TO_BEGINNING, + MOVE_CHILD_TO_END, +}; + +// section of a control, its parent section and the file it is written in +struct SectionRef +{ + KVSection* sec{ nullptr }; + KVSection* parent{ nullptr }; + LayoutDoc* doc{ nullptr }; // not owned, documents are kept until a layout is opened + bool editorOnlyInclude{ false }; // control or its parent comes from an editor-only 'child file' +}; + +// Layout is edited as KeyValues documents, preview controls are recreated from them after every change. +// Controls are matched with their sections by the path of child names from the panel. +class CUIEditor +{ +public: + void Draw(bool& open); + bool LoadDocument(const char* filePath); + void Shutdown(); + +private: + void DestroyPreviewPanel(); + void NewDocument(); + void ResetDocumentState(); + bool SaveDocument(LayoutDoc& doc, const char* filePath); + void SaveIncludes(); + bool IsModified() const; + void OnDocumentChanged(LayoutDoc* doc); + void RebuildPreview(); + void UpdateKVText(); + + void ClearIncludes(); + bool ReloadIncludeDoc(LayoutDoc& doc); + LayoutDoc* GetIncludeDoc(const char* filePath); + LayoutDoc* GetKVDoc(); + + KVSection& GetPanelSection(); + void FindChildSection(KVSection& sec, LayoutDoc* doc, const char* name, SectionRef& result, int depth, bool editorOnlyInclude); + SectionRef FindSectionByPath(const char* path); + EqString GetControlPath(const IUIControl* control) const; + IUIControl* GetSelectedControl() const; + void Select(const char* path); + bool IsEditorHidden(const char* path) const; + void SetEditorHidden(const char* path, bool hidden); + + void AddChildSection(const SectionRef& parentRef, const EqString& parentPath, const char* className, bool editorOnly); + void RemoveChildSection(const EqString& path); + void MoveChildSection(const EqString& path, EMoveChild move); + void CollectLocalFonts(const EqString& path, Array& outNames); + void CollectSectionFonts(const SectionRef& ref, Array& outNames); + + void DrawMenuBar(); + void DrawOpenPopup(); + void DrawSaveAsPopup(); + void DrawAddFilePopup(); + bool DrawResFileList(const char* selectedPath, EqString& outActivated); + void RequestResFileList(); + void AddIncludeSection(const EqString& path, const char* filePath, bool editorOnly); + void DrawOutliner(); + void DrawOutlinerNode(IUIControl* control); + void DrawAddChildMenu(const SectionRef& ref, const EqString& path, bool editorOnly); + void DrawPreviewTab(); + void DrawLayoutPopup(); + void MoveSelectedControl(const IVector2D& move); + void DrawPreviewOverlay(const ImVec2& origin, float zoom, IUIControl* hovered); + void DrawPreviewRulers(const ImVec2& origin, const IVector2D& size, float zoom, const ImVec2& rulerSize); + void RenderPreview(const IVector2D& size); + void DrawKeyValuesTab(); + void DrawInspector(); + void DrawIncludes(const SectionRef& ref); + + LayoutDocPtr m_mainDoc; + Array m_includeDocs{ PP_SL }; + + // preview controls are not added to the manager, they are rendered to own target + Panel* m_previewRoot{ nullptr }; + Panel* m_panel{ nullptr }; + ITexturePtr m_previewTarget; + int m_previewSizeIdx{ 0 }; + int m_zoomPercent{ 100 }; // fixed resolutions only + ColorRGBA m_previewClearColor{ 0.2f, 0.2f, 0.2f, 1.0f }; + bool m_rebuildPending{ true }; + bool m_showBounds{ false }; + bool m_stretchPanel{ true }; + bool m_rootElement{ false }; // layout is rendered without a parent, like the game HUD + + // empty path is the panel itself + EqString m_selection; + bool m_hasSelection{ false }; + + // control of the layout popup opened in the preview + EqString m_layoutPopupPath; + + // outliner item under the mouse in the current frame, outliner is drawn before the preview + EqString m_outlinerHoverPath; + bool m_hasOutlinerHover{ false }; + + // controls hidden in the preview only, not written to the document + Array m_editorHiddenPaths{ PP_SL }; + + // inspector + char m_propertyFilter[64]{}; + + // KeyValues tab, -1 is the main document, otherwise include index + Array m_kvText{ PP_SL }; + int m_kvDocIdx{ -1 }; + bool m_kvTextDirty{ true }; + bool m_kvTextEdited{ false }; + bool m_selectKVTab{ false }; + + // file popups + Array m_resFiles{ PP_SL }; + char m_pathInput[256]{}; + char m_fileFilter[64]{}; + EqString m_addFileTargetPath; // control that gets 'child file' + bool m_addFileEditorOnly{ false }; + bool m_openPopupRequest{ false }; + bool m_saveAsPopupRequest{ false }; + bool m_addFilePopupRequest{ false }; +}; + +static CStaticAutoPtr s_uiEditor; + +//------------------------------------------------------------------------------------------------- + +static void ScanResFiles(const EqString& dirPath, Array& outFiles) +{ + CFileSystemFind fsFind(EqString::Format("%s/*", dirPath), SP_MOD); + while (fsFind.Next()) + { + const EqStringRef name = fsFind.GetPath(); + if (name[0] == '.') + continue; + + const EqString filePath = EqString::Format("%s/%s", dirPath, name); + if (fsFind.IsDirectory()) + { + ScanResFiles(filePath, outFiles); + continue; + } + + if (fnmPathExtractExt(name).CompareCaseIns("res")) + continue; + + // same files may come from several search paths + const int foundIdx = arrayFindIndexF(outFiles, [&](const EqString& file) { + return !file.CompareCaseIns(filePath); + }); + + if (foundIdx == -1) + outFiles.append(filePath); + } +} + +static void RemoveEditorOnlySections(KVSection& sec) +{ + for (int i = sec.KeyCount() - 1; i >= 0; --i) + { + KVSection& key = sec.KeyAt(i); + if (!CString::CompareCaseIns(key.GetName(), "child") && IsEditorOnly(key)) + sec.RemoveSection(&key); + else if (key.KeyCount() > 0) + RemoveEditorOnlySections(key); + } +} + +static bool IsIncludeSection(const KVSection& childSec) +{ + return childSec.ValueCount() > 1 && !CString::CompareCaseIns(childSec.ValueAt(0).GetString(), "file"); +} + +static const char* GetChildSectionName(const KVSection& childSec) +{ + return childSec.ValueCount() > 1 ? childSec.ValueAt(1).GetString() : ""; +} + +static void AddFontNames(const KVSection& sec, Array& outNames) +{ + const KVSection* fontsSec = sec.FindSection("fonts", KV_FLAG_SECTION); + if (!fontsSec) + return; + + for (const KVSection& fontSec : fontsSec->Keys()) + { + const int foundIdx = arrayFindIndexF(outNames, [&](const EqString& name) { + return !name.CompareCaseIns(fontSec.GetName()); + }); + + if (foundIdx == -1) + outNames.append(fontSec.GetName()); + } +} + +// containers and controls with selfVisible off draw nothing +static bool IsDrawnControl(const IUIControl* control) +{ + return control->IsSelfVisible() + && CString::CompareCaseIns(control->GetClassname(), "Container") + && CString::CompareCaseIns(control->GetClassname(), "HudElement"); +} + +// Unlike IUIControl::HitTest, children outside of the parent rectangle are tested, and controls +// that draw nothing are picked only when no drawn control is under the point. +static void EditorHitTest(IUIControl* control, const IVector2D& point, IUIControl*& drawnHit, IUIControl*& anyHit) +{ + if (!control->IsVisible()) + return; + + // first children are drawn on top + for (auto it = control->GetFirstChild(); !it.atEnd() && !drawnHit; ++it) + EditorHitTest(*it, point, drawnHit, anyHit); + + if (drawnHit || !control->GetClientRectangle().Contains(point)) + return; + + if (IsDrawnControl(control)) + drawnHit = control; + else if (!anyHit) + anyHit = control; +} + +static void SetIntPair(KVSection& sec, const char* name, int x, int y) +{ + KVSection& key = sec.CreateSection(name); + key.AddValue(x); + key.AddValue(y); +} + +// path of child names without the last one +static EqString GetParentPath(const EqString& path) +{ + const char* separator = strrchr(path.ToCString(), '.'); + if (!separator) + return EqString(); + return EqString(path.ToCString(), static_cast(separator - path.ToCString())); +} + +static EqString JoinPath(const EqString& parentPath, const char* name) +{ + if (parentPath.Length() == 0) + return EqString(name); + return EqString::Format("%s.%s", parentPath, name); +} + +static int KVTextResizeCb(ImGuiInputTextCallbackData* data) +{ + if (data->EventFlag == ImGuiInputTextFlags_CallbackResize) + { + Array* text = static_cast*>(data->UserData); + text->setNum(data->BufTextLen + 1, false); + data->Buf = text->ptr(); + } + return 0; +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::DestroyPreviewPanel() +{ + // panel is owned by the preview root unless it is the root element + if (m_panel && !m_panel->GetParent()) + delete m_panel; + m_panel = nullptr; + + if (m_previewRoot) + m_previewRoot->ClearChilds(true); +} + +void CUIEditor::Shutdown() +{ + DestroyPreviewPanel(); + SAFE_DELETE(m_previewRoot); + ClearIncludes(); + m_mainDoc = nullptr; + m_previewTarget = nullptr; + m_rebuildPending = true; +} + +void CUIEditor::ResetDocumentState() +{ + ClearIncludes(); + + // layouts without the panel section are loaded into a root element (HUD), others are added as panels + m_rootElement = m_mainDoc->root.FindSection("panel", KV_FLAG_SECTION) == nullptr; + + m_mainDoc->modified = false; + m_rebuildPending = true; + m_kvDocIdx = -1; + m_kvTextDirty = true; + m_kvTextEdited = false; + m_editorHiddenPaths.clear(); + Select(""); +} + +void CUIEditor::NewDocument() +{ + m_mainDoc = CRefPtr_new(LayoutDoc); + + KVSection& panelSec = m_mainDoc->root.CreateSection("panel", "NewPanel"); + SetIntPair(panelSec, "position", 0, 0); + SetIntPair(panelSec, "size", 640, 480); + + ResetDocumentState(); +} + +bool CUIEditor::LoadDocument(const char* filePath) +{ + IFileStreamPtr file = g_fileSystem->Open(filePath, FS_OPEN_READ, SP_MOD); + LayoutDocPtr doc = CRefPtr_new(LayoutDoc); + if (!file || !KeyValues::Parse(file, doc->root)) + { + MsgError("EqUI editor: can't load '%s'\n", filePath); + return false; + } + + doc->filePath = filePath; + m_mainDoc = doc; + + ResetDocumentState(); + return true; +} + +bool CUIEditor::SaveDocument(LayoutDoc& doc, const char* filePath) +{ + KVSection document; + doc.root.CopyTo(document); + RemoveEditorOnlySections(document); + + IFileStreamPtr file = g_fileSystem->Open(filePath, FS_OPEN_WRITE, SP_MOD); + if (!file) + { + MsgError("EqUI editor: can't write '%s'\n", filePath); + return false; + } + + KeyValues::WriteText(file, document); + + doc.filePath = filePath; + doc.modified = false; + Msg("EqUI editor: saved '%s'\n", filePath); + return true; +} + +void CUIEditor::SaveIncludes() +{ + for (const LayoutDocPtr& doc : m_includeDocs) + { + if (doc->modified) + SaveDocument(*doc, EqString(doc->filePath)); + } +} + +bool CUIEditor::IsModified() const +{ + if (m_mainDoc->modified) + return true; + + for (const LayoutDocPtr& doc : m_includeDocs) + { + if (doc->modified) + return true; + } + return false; +} + +void CUIEditor::OnDocumentChanged(LayoutDoc* doc) +{ + if (doc) + doc->modified = true; + + m_rebuildPending = true; + m_kvTextDirty = true; +} + +void CUIEditor::RebuildPreview() +{ + m_rebuildPending = false; + + if (!m_previewRoot) + { + // stands for the manager root panel which covers the view frame + m_previewRoot = Manager->Create(); + m_previewRoot->SetName("equi_editor_root"); + m_previewRoot->SetSelfVisible(false); + } + + DestroyPreviewPanel(); + + m_panel = Manager->Create(); + if (!m_rootElement) + m_previewRoot->AddChild(m_panel); + + // included files come from the editor so their changes are shown before saving + Manager->SetLayoutFileLoader([this](const char* fileName, KVSection& outSec) { + const LayoutDoc* doc = GetIncludeDoc(fileName); + if (!doc) + return false; + + doc->root.CopyTo(outSec); + return true; + }); + + m_panel->InitFromKeyValues(m_mainDoc->root); + + Manager->SetLayoutFileLoader(nullptr); + + // window panels are hidden after parsing + m_panel->Show(); + + // root elements are containers in the game, they don't draw themselves + if (m_rootElement) + m_panel->SetSelfVisible(false); + + for (const EqString& path : m_editorHiddenPaths) + { + IUIControl* control = path.Length() ? m_panel->Get(path) : m_panel; + if (control) + control->Hide(); + } +} + +void CUIEditor::UpdateKVText() +{ + m_kvTextDirty = false; + + CMemoryStream stream(PP_SL); + stream.Open(FS_OPEN_WRITE, nullptr, 4096); + KeyValues::WriteText(&stream, GetKVDoc()->root); + + const int textLength = static_cast(stream.Tell()); + m_kvText.setNum(textLength + 1, false); + memcpy(m_kvText.ptr(), stream.GetBasePointer(), textLength); + m_kvText[textLength] = 0; +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::ClearIncludes() +{ + m_includeDocs.clear(); +} + +// discards changes of the file, the file that failed to load is loaded again +bool CUIEditor::ReloadIncludeDoc(LayoutDoc& doc) +{ + IFileStreamPtr file = g_fileSystem->Open(doc.filePath, FS_OPEN_READ, SP_MOD); + KVSection root; + if (!file || !KeyValues::Parse(file, root)) + { + MsgError("EqUI editor: can't reload '%s'\n", doc.filePath.ToCString()); + return false; + } + + doc.root.Clear(); + root.CopyTo(doc.root); + doc.modified = false; + doc.valid = true; + + if (GetKVDoc() == &doc) + m_kvTextEdited = false; + + m_rebuildPending = true; + m_kvTextDirty = true; + return true; +} + +LayoutDoc* CUIEditor::GetIncludeDoc(const char* filePath) +{ + for (const LayoutDocPtr& doc : m_includeDocs) + { + if (!doc->filePath.CompareCaseIns(filePath)) + return doc->valid ? doc.Ptr() : nullptr; + } + + LayoutDocPtr doc = CRefPtr_new(LayoutDoc); + doc->filePath = filePath; + m_includeDocs.append(doc); + + // same as CUIManager::LoadLayoutFile + IFileStreamPtr file = g_fileSystem->Open(filePath, FS_OPEN_READ, SP_MOD); + if (!file || !KeyValues::Parse(file, doc->root)) + { + MsgWarning("EqUI editor: can't load included file '%s'\n", filePath); + doc->valid = false; + return nullptr; + } + + return doc.Ptr(); +} + +LayoutDoc* CUIEditor::GetKVDoc() +{ + if (m_includeDocs.inRange(m_kvDocIdx) && m_includeDocs[m_kvDocIdx]->valid) + return m_includeDocs[m_kvDocIdx].Ptr(); + return m_mainDoc.Ptr(); +} + +KVSection& CUIEditor::GetPanelSection() +{ + // same as Panel::InitFromKeyValues + KVSection* panelSec = m_mainDoc->root.FindSection("panel", KV_FLAG_SECTION); + return panelSec ? *panelSec : m_mainDoc->root; +} + +// same order as IUIControl::InitChildItems, later section with the same name overrides the control +void CUIEditor::FindChildSection(KVSection& sec, LayoutDoc* doc, const char* name, SectionRef& result, int depth, bool editorOnlyInclude) +{ + for (KVSection& childSec : sec.Keys("child")) + { + if (IsIncludeSection(childSec)) + { + LayoutDoc* includeDoc = depth < s_maxIncludeDepth ? GetIncludeDoc(GetChildSectionName(childSec)) : nullptr; + if (includeDoc) + FindChildSection(includeDoc->root, includeDoc, name, result, depth + 1, editorOnlyInclude || IsEditorOnly(childSec)); + continue; + } + + if (!CString::Compare(GetChildSectionName(childSec), name)) + result = SectionRef{ &childSec, &sec, doc, editorOnlyInclude }; + } +} + +SectionRef CUIEditor::FindSectionByPath(const char* path) +{ + SectionRef ref{ &GetPanelSection(), nullptr, m_mainDoc.Ptr() }; + + EqString name; + for (const char* c = path; ; ++c) + { + if (*c && *c != '.') + { + name.Append(*c); + continue; + } + + if (name.Length()) + { + SectionRef childRef; + FindChildSection(*ref.sec, ref.doc, name, childRef, 0, false); + if (!childRef.sec) + return SectionRef(); + + // children of controls from editor-only files are removed with them + childRef.editorOnlyInclude = childRef.editorOnlyInclude || ref.editorOnlyInclude; + ref = childRef; + name.Clear(); + } + + if (!*c) + break; + } + return ref; +} + +EqString CUIEditor::GetControlPath(const IUIControl* control) const +{ + EqString path; + for (; control && control != m_panel; control = control->GetParent()) + { + if (path.Length()) + path = EqString::Format("%s.%s", control->GetName(), path); + else + path = control->GetName(); + } + return path; +} + +IUIControl* CUIEditor::GetSelectedControl() const +{ + if (!m_hasSelection || !m_panel) + return nullptr; + + if (m_selection.Length() == 0) + return m_panel; + + return m_panel->Get(m_selection); +} + +void CUIEditor::Select(const char* path) +{ + m_selection = path; + m_hasSelection = true; +} + +bool CUIEditor::IsEditorHidden(const char* path) const +{ + return arrayFindIndex(m_editorHiddenPaths, EqString(path)) != -1; +} + +void CUIEditor::SetEditorHidden(const char* path, bool hidden) +{ + const int pathIdx = arrayFindIndex(m_editorHiddenPaths, EqString(path)); + if (hidden && pathIdx == -1) + m_editorHiddenPaths.append(path); + else if (!hidden && pathIdx != -1) + m_editorHiddenPaths.fastRemoveIndex(pathIdx); + + // rebuild restores visibility from the layout + m_rebuildPending = true; +} + +void CUIEditor::AddChildSection(const SectionRef& parentRef, const EqString& parentPath, const char* className, bool editorOnly) +{ + KVSection& parentSec = *parentRef.sec; + + EqString name; + for (int i = 1; ; ++i) + { + name = EqString::Format("%s%d", className, i); + + bool nameUsed = false; + for (const KVSection& key : parentSec.Keys("child")) + nameUsed = nameUsed || !CString::Compare(GetChildSectionName(key), name); + + if (!nameUsed) + break; + } + + KVSection& childSec = parentSec.CreateSection("child", className); + childSec.AddValue(name); + SetIntPair(childSec, "position", 10, 10); + SetIntPair(childSec, "size", 100, 30); + + if (!CString::CompareCaseIns(className, "label") || !CString::CompareCaseIns(className, "button")) + childSec.SetKey("label", name); + + if (editorOnly) + childSec.SetKey(EDITOR_ONLY_KEY, 1); + + Select(JoinPath(parentPath, name)); + OnDocumentChanged(parentRef.doc); +} + +void CUIEditor::RemoveChildSection(const EqString& path) +{ + const SectionRef ref = FindSectionByPath(path); + if (!ref.sec || !ref.parent) + return; + + ref.parent->RemoveSection(ref.sec); + + // selection inside of the removed control + const EqString childPrefix = EqString::Format("%s.", path); + if (m_hasSelection && (m_selection == path || !strncmp(m_selection, childPrefix, childPrefix.Length()))) + Select(GetParentPath(path)); + + OnDocumentChanged(ref.doc); +} + +void CUIEditor::MoveChildSection(const EqString& path, EMoveChild move) +{ + const SectionRef ref = FindSectionByPath(path); + if (!ref.sec || !ref.parent) + return; + + Array& keys = ref.parent->keys; + const int idx = arrayFindIndex(keys, ref.sec); + if (idx == -1) + return; + + // only child keys are passed, other keys stay in place + const auto findChildKey = [&](int from, int step) { + for (int i = from + step; i >= 0 && i < keys.numElem(); i += step) + { + if (!CString::CompareCaseIns(keys[i]->GetName(), "child")) + return i; + } + return -1; + }; + + switch (move) + { + case MOVE_CHILD_UP: + case MOVE_CHILD_DOWN: + { + const int otherIdx = findChildKey(idx, move == MOVE_CHILD_UP ? 1 : -1); + if (otherIdx == -1) + return; + + keys[idx] = keys[otherIdx]; + keys[otherIdx] = ref.sec; + break; + } + case MOVE_CHILD_TO_BEGINNING: + { + if (findChildKey(idx, 1) == -1) + return; + + keys.removeIndex(idx); + keys.append(ref.sec); + break; + } + case MOVE_CHILD_TO_END: + { + const int firstIdx = findChildKey(-1, 1); + if (firstIdx == idx) + return; + + keys.removeIndex(idx); + keys.insert(ref.sec, firstIdx); + break; + } + } + + OnDocumentChanged(ref.doc); +} + +void CUIEditor::CollectSectionFonts(const SectionRef& ref, Array& outNames) +{ + if (!ref.sec) + return; + + AddFontNames(*ref.sec, outNames); + + // included files add their fonts to the control that includes them + for (const KVSection& childSec : ref.sec->Keys("child")) + { + if (!IsIncludeSection(childSec)) + continue; + + if (const LayoutDoc* includeDoc = GetIncludeDoc(GetChildSectionName(childSec))) + AddFontNames(includeDoc->root, outNames); + } +} + +// named fonts are searched in the control and its parents +void CUIEditor::CollectLocalFonts(const EqString& path, Array& outNames) +{ + CollectSectionFonts(FindSectionByPath(""), outNames); + + for (int i = 1; i <= path.Length(); ++i) + { + if (i == path.Length() || path[i] == '.') + CollectSectionFonts(FindSectionByPath(EqString(path.ToCString(), i)), outNames); + } + + arraySort(outNames, [](const EqString& a, const EqString& b) { + return a.CompareCaseIns(b); + }); +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::Draw(bool& open) +{ + if (!open) + return; + + if (m_rebuildPending) + { + if (!m_mainDoc) + NewDocument(); + RebuildPreview(); + } + + if (m_kvTextDirty && !m_kvTextEdited) + UpdateKVText(); + + const char* fileName = m_mainDoc->filePath.Length() ? m_mainDoc->filePath.ToCString() : "untitled"; + const EqString title = EqString::Format("EqUI Editor - %s%s###equiEditor", fileName, IsModified() ? "*" : ""); + + ImGui::SetNextWindowSize(ImVec2(1280, 720), ImGuiCond_FirstUseEver); + if (!ImGui::Begin(title, &open, ImGuiWindowFlags_MenuBar)) + { + ImGui::End(); + return; + } + + DrawMenuBar(); + DrawOpenPopup(); + DrawSaveAsPopup(); + DrawAddFilePopup(); + + const float layoutHeight = ImGui::GetContentRegionAvail().y; + constexpr ImGuiTableFlags layoutFlags = ImGuiTableFlags_Resizable | ImGuiTableFlags_BordersInnerV; + if (ImGui::BeginTable("##equiEditorLayout", 3, layoutFlags)) + { + ImGui::TableSetupColumn("Outliner", ImGuiTableColumnFlags_WidthFixed, 260.0f); + ImGui::TableSetupColumn("View", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Inspector", ImGuiTableColumnFlags_WidthFixed, 360.0f); + ImGui::TableNextRow(); + + ImGui::TableNextColumn(); + if (ImGui::BeginChild("##outliner", ImVec2(0, layoutHeight))) + DrawOutliner(); + ImGui::EndChild(); + + ImGui::TableNextColumn(); + if (ImGui::BeginChild("##view", ImVec2(0, layoutHeight))) + { + if (ImGui::BeginTabBar("##viewTabs")) + { + if (ImGui::BeginTabItem("Preview")) + { + DrawPreviewTab(); + ImGui::EndTabItem(); + } + + const ImGuiTabItemFlags kvTabFlags = m_selectKVTab ? ImGuiTabItemFlags_SetSelected : 0; + m_selectKVTab = false; + if (ImGui::BeginTabItem("KeyValues", nullptr, kvTabFlags)) + { + DrawKeyValuesTab(); + ImGui::EndTabItem(); + } + ImGui::EndTabBar(); + } + } + ImGui::EndChild(); + + ImGui::TableNextColumn(); + if (ImGui::BeginChild("##inspector", ImVec2(0, layoutHeight))) + DrawInspector(); + ImGui::EndChild(); + + ImGui::EndTable(); + } + + ImGui::End(); +} + +void CUIEditor::DrawMenuBar() +{ + if (!ImGui::BeginMenuBar()) + return; + + if (ImGui::BeginMenu("File")) + { + if (ImGui::MenuItem("New")) + NewDocument(); + + if (ImGui::MenuItem("Open...")) + m_openPopupRequest = true; + + if (ImGui::MenuItem("Reload", nullptr, false, m_mainDoc->filePath.Length() > 0)) + LoadDocument(EqString(m_mainDoc->filePath)); + + if (ImGui::MenuItem("Reload included files", nullptr, false, m_includeDocs.numElem() > 0)) + { + for (const LayoutDocPtr& doc : m_includeDocs) + ReloadIncludeDoc(*doc); + } + + ImGui::Separator(); + + // included files are saved together with the layout + if (ImGui::MenuItem("Save", nullptr, false, IsModified())) + { + if (m_mainDoc->filePath.Length()) + { + if (m_mainDoc->modified) + SaveDocument(*m_mainDoc, EqString(m_mainDoc->filePath)); + SaveIncludes(); + } + else + { + m_saveAsPopupRequest = true; + } + } + + if (ImGui::MenuItem("Save As...")) + m_saveAsPopupRequest = true; + + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("View")) + { + ImGui::MenuItem("Show control bounds", nullptr, &m_showBounds); + ImGui::ColorEdit4("Background", &m_previewClearColor.x, ImGuiColorEditFlags_NoInputs); + ImGui::EndMenu(); + } + + ImGui::EndMenuBar(); +} + +void CUIEditor::RequestResFileList() +{ + m_resFiles.clear(); + ScanResFiles(s_resourcesDir, m_resFiles); + arraySort(m_resFiles, [](const EqString& a, const EqString& b) { + return a.CompareCaseIns(b); + }); +} + +// filtered list of .res files with the path input, returns true when a file is double-clicked +bool CUIEditor::DrawResFileList(const char* selectedPath, EqString& outActivated) +{ + bool activated = false; + + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputTextWithHint("##filter", "Filter", m_fileFilter, sizeof(m_fileFilter)); + + const float bottomHeight = ImGui::GetFrameHeightWithSpacing() * 2.0f; + if (ImGui::BeginChild("##files", ImVec2(0, -bottomHeight), true)) + { + for (const EqString& file : m_resFiles) + { + if (m_fileFilter[0] && file.Find(m_fileFilter) == -1) + continue; + + if (ImGui::Selectable(file, !file.CompareCaseIns(selectedPath), ImGuiSelectableFlags_AllowDoubleClick)) + { + strncpy(m_pathInput, file.ToCString(), sizeof(m_pathInput) - 1); + if (ImGui::IsMouseDoubleClicked(0)) + { + outActivated = file; + activated = true; + } + } + } + } + ImGui::EndChild(); + + ImGui::SetNextItemWidth(-FLT_MIN); + ImGui::InputText("##path", m_pathInput, sizeof(m_pathInput)); + + return activated; +} + +void CUIEditor::DrawOpenPopup() +{ + if (m_openPopupRequest) + { + m_openPopupRequest = false; + RequestResFileList(); + + strncpy(m_pathInput, m_mainDoc->filePath.ToCString(), sizeof(m_pathInput) - 1); + ImGui::OpenPopup("Open layout"); + } + + ImGui::SetNextWindowSize(ImVec2(500, 500), ImGuiCond_Appearing); + if (!ImGui::BeginPopupModal("Open layout")) + return; + + EqString activatedFile; + if (DrawResFileList(m_pathInput, activatedFile) && LoadDocument(activatedFile)) + ImGui::CloseCurrentPopup(); + + if (ImGui::Button("Open") && LoadDocument(m_pathInput)) + ImGui::CloseCurrentPopup(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); +} + +void CUIEditor::DrawSaveAsPopup() +{ + if (m_saveAsPopupRequest) + { + m_saveAsPopupRequest = false; + strncpy(m_pathInput, m_mainDoc->filePath.Length() ? m_mainDoc->filePath.ToCString() : "resources/", sizeof(m_pathInput) - 1); + ImGui::OpenPopup("Save layout"); + } + + if (!ImGui::BeginPopupModal("Save layout", nullptr, ImGuiWindowFlags_AlwaysAutoResize)) + return; + + ImGui::TextUnformatted("File path (SP_MOD):"); + ImGui::SetNextItemWidth(400.0f); + ImGui::InputText("##path", m_pathInput, sizeof(m_pathInput)); + + if (ImGui::Button("Save") && SaveDocument(*m_mainDoc, m_pathInput)) + { + SaveIncludes(); + ImGui::CloseCurrentPopup(); + } + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); +} + +void CUIEditor::DrawAddFilePopup() +{ + if (m_addFilePopupRequest) + { + m_addFilePopupRequest = false; + RequestResFileList(); + + m_pathInput[0] = 0; + ImGui::OpenPopup("Add child file"); + } + + ImGui::SetNextWindowSize(ImVec2(500, 500), ImGuiCond_Appearing); + if (!ImGui::BeginPopupModal("Add child file")) + return; + + ImGui::Text("Add to: %s", m_addFileTargetPath.Length() ? m_addFileTargetPath.ToCString() : "(panel)"); + ImGui::Checkbox("Editor only", &m_addFileEditorOnly); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Controls of the file are shown in the preview only, the entry is removed on save"); + + EqString activatedFile; + if (DrawResFileList(m_pathInput, activatedFile)) + { + AddIncludeSection(m_addFileTargetPath, activatedFile, m_addFileEditorOnly); + ImGui::CloseCurrentPopup(); + } + + ImGui::BeginDisabled(m_pathInput[0] == 0); + if (ImGui::Button("Add")) + { + AddIncludeSection(m_addFileTargetPath, m_pathInput, m_addFileEditorOnly); + ImGui::CloseCurrentPopup(); + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (ImGui::Button("Cancel")) + ImGui::CloseCurrentPopup(); + + ImGui::EndPopup(); +} + +void CUIEditor::AddIncludeSection(const EqString& path, const char* filePath, bool editorOnly) +{ + const SectionRef ref = FindSectionByPath(path); + if (!ref.sec) + return; + + // file that includes itself would be loaded recursively by IUIControl::InitChildItems + if (!m_mainDoc->filePath.CompareCaseIns(filePath) || !ref.doc->filePath.CompareCaseIns(filePath)) + { + MsgError("EqUI editor: '%s' can't include itself\n", filePath); + return; + } + + KVSection& includeSec = ref.sec->CreateSection("child", "file"); + includeSec.AddValue(filePath); + + if (editorOnly) + includeSec.SetKey(EDITOR_ONLY_KEY, 1); + + OnDocumentChanged(ref.doc); +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::DrawOutliner() +{ + ImGui::TextUnformatted("Outliner"); + ImGui::Separator(); + + m_hasOutlinerHover = false; + + if (m_panel) + DrawOutlinerNode(m_panel); +} + +void CUIEditor::DrawOutlinerNode(IUIControl* control) +{ + const EqString path = GetControlPath(control); + const SectionRef ref = FindSectionByPath(path); + const bool isPanel = control == m_panel; + const bool sectionEditorOnly = ref.sec && IsEditorOnly(*ref.sec); + const bool editorOnly = sectionEditorOnly || ref.editorOnlyInclude; + const bool fromInclude = ref.doc && ref.doc != m_mainDoc.Ptr(); + + ImGuiTreeNodeFlags flags = ImGuiTreeNodeFlags_OpenOnArrow | ImGuiTreeNodeFlags_SpanAvailWidth; + if (isPanel) + flags |= ImGuiTreeNodeFlags_DefaultOpen; + if (control->GetFirstChild().atEnd()) + flags |= ImGuiTreeNodeFlags_Leaf; + if (m_hasSelection && m_selection == path) + flags |= ImGuiTreeNodeFlags_Selected; + + // controls without a section are created by the control itself, e.g. window controls + const bool hasTextColor = editorOnly || fromInclude || !ref.sec; + if (editorOnly) + ImGui::PushStyleColor(ImGuiCol_Text, s_editorOnlyTextColor); + else if (fromInclude) + ImGui::PushStyleColor(ImGuiCol_Text, s_includeTextColor); + else if (!ref.sec) + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::GetStyleColorVec4(ImGuiCol_TextDisabled)); + + bool editorVisible = !IsEditorHidden(path); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + if (ImGui::Checkbox(EqString::Format("##editorVisible%s", path), &editorVisible)) + SetEditorHidden(path, !editorVisible); + ImGui::PopStyleVar(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Show in the preview, not saved to the layout"); + + ImGui::SameLine(); + + // ids are paths so the tree state is kept when the preview is rebuilt + const bool nodeOpen = ImGui::TreeNodeEx(EqString::Format("#%s", path), flags, "%s [%s]%s%s%s", + control->GetName(), control->GetClassname(), + editorOnly ? " (editor only)" : "", + fromInclude ? " - " : "", fromInclude ? fnmPathExtractName(ref.doc->filePath).ToCString() : ""); + + if (hasTextColor) + ImGui::PopStyleColor(); + + if (ImGui::IsItemHovered()) + { + m_outlinerHoverPath = path; + m_hasOutlinerHover = true; + } + + if (ImGui::IsItemClicked() && !ImGui::IsItemToggledOpen()) + Select(path); + + if (ref.sec && ImGui::BeginPopupContextItem()) + { + if (ImGui::BeginMenu("Add child")) + { + DrawAddChildMenu(ref, path, false); + ImGui::EndMenu(); + } + + if (ImGui::BeginMenu("Add editor-only child")) + { + DrawAddChildMenu(ref, path, true); + ImGui::EndMenu(); + } + + if (ImGui::MenuItem("Add child file...")) + { + m_addFileTargetPath = path; + m_addFilePopupRequest = true; + } + + if (fromInclude && ImGui::MenuItem(EqString::Format("Reload %s", fnmPathExtractName(ref.doc->filePath).ToCString()))) + ReloadIncludeDoc(*ref.doc); + + ImGui::Separator(); + if (ImGui::BeginMenu("Layout")) + { + if (DrawLayoutProperties(*ref.sec)) + OnDocumentChanged(ref.doc); + ImGui::EndMenu(); + } + + if (!isPanel) + { + ImGui::Separator(); + + static const char* s_moveItems[] = { "Move up", "Move down", "Move to beginning", "Move to end" }; + for (int i = 0; i < static_cast(elementsOf(s_moveItems)); ++i) + { + if (ImGui::MenuItem(s_moveItems[i])) + MoveChildSection(path, static_cast(i)); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Upper controls are drawn on top and written later in the file"); + } + + ImGui::Separator(); + + bool editorOnlyValue = sectionEditorOnly; + if (ImGui::MenuItem("Editor only", nullptr, &editorOnlyValue)) + { + if (editorOnlyValue) + ref.sec->SetKey(EDITOR_ONLY_KEY, 1); + else + ref.sec->RemoveSectionByName(EDITOR_ONLY_KEY); + OnDocumentChanged(ref.doc); + } + + if (ImGui::MenuItem("Delete")) + RemoveChildSection(path); + } + ImGui::EndPopup(); + } + + if (!nodeOpen) + return; + + for (auto it = control->GetFirstChild(); !it.atEnd(); ++it) + DrawOutlinerNode(*it); + + ImGui::TreePop(); +} + +void CUIEditor::DrawAddChildMenu(const SectionRef& ref, const EqString& path, bool editorOnly) +{ + // engine and game controls registered in the manager + static Array classNames{ PP_SL }; + classNames.clear(); + + Manager->GetControlClassNames(classNames); + arraySort(classNames, [](const char* a, const char* b) { + return CString::CompareCaseIns(a, b); + }); + + for (EqStringRef className : classNames) + { + if (ImGui::MenuItem(className)) + AddChildSection(ref, path, className, editorOnly); + } +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::RenderPreview(const IVector2D& size) +{ + if (!m_previewTarget) + { + // filtered for zoom + const SamplerStateParams sampler(TEXFILTER_LINEAR, TEXADDRESS_CLAMP); + m_previewTarget = g_renderAPI->CreateRenderTarget( + Builder() + .Name("_equi_editor_preview") + .Size(size.x, size.y) + .Format(g_matSystem->GetCurrentBackbuffer()->GetFormat()) + .Sampler(sampler) + .End() + ); + } + else if (m_previewTarget->GetWidth() != size.x || m_previewTarget->GetHeight() != size.y) + { + g_renderAPI->ResizeRenderTarget(m_previewTarget, { size.x, size.y, 1 }); + } + + // screen overlay panels are resized by CUIManager::Render, HUD layouts are resized by the game + if (m_stretchPanel || KV_GetValueBool(GetPanelSection().FindSection("screenoverlay"), 0, false)) + { + m_panel->SetPosition(0); + m_panel->SetSize(size); + } + + // parsed size is the base size, so root does not scale its children like the manager root + if (m_previewRoot->GetSize() != size) + { + KVSection rootSec; + SetIntPair(rootSec, "position", 0, 0); + SetIntPair(rootSec, "size", size.x, size.y); + m_previewRoot->Parse(rootSec); + } + + // editor is drawn in the middle of the host frame + Matrix4x4 prevProj, prevView, prevWorld, prevWorld2; + g_matSystem->GetMatrix(MATRIXMODE_PROJECTION, prevProj); + g_matSystem->GetMatrix(MATRIXMODE_VIEW, prevView); + g_matSystem->GetMatrix(MATRIXMODE_WORLD, prevWorld); + g_matSystem->GetMatrix(MATRIXMODE_WORLD2, prevWorld2); + + g_matSystem->Setup2D(size.x, size.y); + + IGPURenderPassRecorderPtr rendPassRecorder = g_renderAPI->BeginRenderPass( + Builder() + .ColorTarget(m_previewTarget, true, m_previewClearColor) + .End() + ); + + ControlRenderContext ctrlRenderContext{ rendPassRecorder }; + if (m_rootElement) + m_panel->Render(1, ctrlRenderContext); + else + m_previewRoot->Render(1, ctrlRenderContext); + + g_matSystem->QueueCommandBuffer(rendPassRecorder->End()); + + g_matSystem->SetMatrix(MATRIXMODE_PROJECTION, prevProj); + g_matSystem->SetMatrix(MATRIXMODE_VIEW, prevView); + g_matSystem->SetMatrix(MATRIXMODE_WORLD, prevWorld); + g_matSystem->SetMatrix(MATRIXMODE_WORLD2, prevWorld2); +} + +void CUIEditor::DrawPreviewTab() +{ + ImGui::SetNextItemWidth(150.0f); + if (ImGui::BeginCombo("Size", s_previewSizes[m_previewSizeIdx].name)) + { + for (int i = 0; i < static_cast(elementsOf(s_previewSizes)); ++i) + { + if (ImGui::Selectable(s_previewSizes[i].name, i == m_previewSizeIdx)) + m_previewSizeIdx = i; + } + ImGui::EndCombo(); + } + ImGui::SameLine(); + if (ImGui::Checkbox("Root element", &m_rootElement)) + m_rebuildPending = true; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Render the layout without a parent and without its own background, like the game HUD.\nOff: the layout is a panel added to the manager root panel."); + + ImGui::SameLine(); + ImGui::Checkbox("Stretch panel", &m_stretchPanel); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Resize the panel to the preview size, children are scaled from the layout size"); + + ImGui::SameLine(); + ImGui::Checkbox("Bounds", &m_showBounds); + + IVector2D size = s_previewSizes[m_previewSizeIdx].size; + const bool fitToWindow = size.x == 0; + + ImGui::SameLine(); + ImGui::BeginDisabled(fitToWindow); + ImGui::SetNextItemWidth(150.0f); + ImGui::SliderInt("Zoom", &m_zoomPercent, s_minZoomPercent, s_maxZoomPercent, "%d%%"); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Ctrl + mouse wheel over the preview, double-click resets to 100%%"); + if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) + m_zoomPercent = 100; + ImGui::EndDisabled(); + + // mouse wheel with Ctrl zooms instead of scrolling + const ImGuiWindowFlags areaFlags = ImGuiWindowFlags_HorizontalScrollbar | (ImGui::GetIO().KeyCtrl ? ImGuiWindowFlags_NoScrollWithMouse : 0); + if (!ImGui::BeginChild("##previewArea", ImVec2(0, 0), false, areaFlags)) + { + ImGui::EndChild(); + return; + } + + const ImVec2 availSize = ImGui::GetContentRegionAvail(); + if (fitToWindow) + size = IVector2D(max(static_cast(availSize.x), 1), max(static_cast(availSize.y), 1)); + + const float zoom = fitToWindow ? 1.0f : m_zoomPercent / 100.0f; + const ImVec2 displaySize(floorf(size.x * zoom), floorf(size.y * zoom)); + + if (m_panel) + { + RenderPreview(size); + + ImVec2 origin = ImGui::GetCursorScreenPos(); + ImVec2 rulerSize(0.0f, 0.0f); + + // fixed resolution is centered in the area, rulers are on the top and left sides + if (!fitToWindow) + { + rulerSize = ImVec2(ImGui::CalcTextSize("0000").x + 6.0f, ImGui::GetFontSize() + 4.0f); + + const ImVec2 blockSize(rulerSize.x + displaySize.x, rulerSize.y + displaySize.y); + const ImVec2 offset(max((availSize.x - blockSize.x) * 0.5f, 0.0f), max((availSize.y - blockSize.y) * 0.5f, 0.0f)); + origin = ImVec2(floorf(origin.x + offset.x + rulerSize.x), floorf(origin.y + offset.y + rulerSize.y)); + ImGui::SetCursorScreenPos(origin); + } + + // preview target alpha is not coverage + ImGui_ImplMatSystem_BeginOpaque(ImGui::GetWindowDrawList()); + ImGui::Image(reinterpret_cast(m_previewTarget.Ptr()), displaySize); + ImGui_ImplMatSystem_EndOpaque(ImGui::GetWindowDrawList()); + const bool previewHovered = ImGui::IsItemHovered(); + const bool previewClicked = ImGui::IsItemClicked(ImGuiMouseButton_Left); + + const ImGuiIO& io = ImGui::GetIO(); + if (previewHovered && !fitToWindow && io.KeyCtrl && io.MouseWheel != 0.0f) + m_zoomPercent = clamp(m_zoomPercent + static_cast(io.MouseWheel) * 10, s_minZoomPercent, s_maxZoomPercent); + + IUIControl* hovered = nullptr; + if (previewHovered) + { + const ImVec2 mousePos = ImGui::GetMousePos(); + const IVector2D point(static_cast((mousePos.x - origin.x) / zoom), static_cast((mousePos.y - origin.y) / zoom)); + + IUIControl* drawnHit = nullptr; + IUIControl* anyHit = nullptr; + EditorHitTest(m_panel, point, drawnHit, anyHit); + hovered = drawnHit ? drawnHit : anyHit; + } + + if (hovered && previewClicked) + Select(GetControlPath(hovered)); + + if (hovered && ImGui::IsItemClicked(ImGuiMouseButton_Right)) + { + Select(GetControlPath(hovered)); + m_layoutPopupPath = m_selection; + ImGui::OpenPopup("##controlLayout"); + } + + if (hovered && !ImGui::IsPopupOpen("##controlLayout")) + ImGui::SetTooltip("%s [%s]", hovered->GetName(), hovered->GetClassname()); + + DrawLayoutPopup(); + + // arrows move the selected control, Shift moves by 10 + if (ImGui::IsWindowFocused() && !io.WantTextInput) + { + const int step = io.KeyShift ? 10 : 1; + const IVector2D move( + (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_RightArrow)) ? step : 0) - (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_LeftArrow)) ? step : 0), + (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_DownArrow)) ? step : 0) - (ImGui::IsKeyPressed(ImGui::GetKeyIndex(ImGuiKey_UpArrow)) ? step : 0)); + + if (move.x || move.y) + MoveSelectedControl(move); + } + + // control under the mouse in the outliner is highlighted when visible + if (!hovered && m_hasOutlinerHover) + { + IUIControl* outlinerHovered = m_outlinerHoverPath.Length() ? m_panel->Get(m_outlinerHoverPath) : m_panel; + if (outlinerHovered && outlinerHovered->IsVisible()) + hovered = outlinerHovered; + } + + DrawPreviewOverlay(origin, zoom, hovered); + + if (!fitToWindow) + DrawPreviewRulers(origin, size, zoom, rulerSize); + } + + ImGui::EndChild(); +} + +void CUIEditor::DrawLayoutPopup() +{ + if (!ImGui::BeginPopup("##controlLayout")) + return; + + // path is kept because the controls are recreated after every change + const SectionRef ref = FindSectionByPath(m_layoutPopupPath); + IUIControl* control = m_layoutPopupPath.Length() ? m_panel->Get(m_layoutPopupPath) : m_panel; + + if (control) + ImGui::Text("%s [%s]", control->GetName(), control->GetClassname()); + + ImGui::Separator(); + + if (ref.sec) + { + if (DrawLayoutProperties(*ref.sec)) + OnDocumentChanged(ref.doc); + } + else + { + ImGui::TextDisabled("Control has no layout section"); + } + + ImGui::EndPopup(); +} + +// position is the offset from the right and bottom sides with these alignments +void CUIEditor::MoveSelectedControl(const IVector2D& move) +{ + const IUIControl* control = GetSelectedControl(); + const SectionRef ref = FindSectionByPath(m_selection); + if (!control || !ref.sec) + return; + + const int alignment = control->GetAlignment(); + const IVector2D offset( + (alignment & UI_ALIGN_RIGHT) ? -move.x : move.x, + (alignment & UI_ALIGN_BOTTOM) ? -move.y : move.y); + + KVSection* key = ref.sec->FindSection("position"); + IVector2D position(0); + if (key) + key->GetValues(position); + else + key = &ref.sec->CreateSection("position"); + + position += offset; + key->ClearValues(); + key->AddValue(position.x); + key->AddValue(position.y); + + OnDocumentChanged(ref.doc); +} + +void CUIEditor::DrawPreviewRulers(const ImVec2& origin, const IVector2D& size, float zoom, const ImVec2& rulerSize) +{ + const ImVec2 displaySize(floorf(size.x * zoom), floorf(size.y * zoom)); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + const ImU32 backColor = ImGui::GetColorU32(ImGuiCol_FrameBg); + const ImU32 tickColor = ImGui::GetColorU32(ImGuiCol_TextDisabled); + const ImU32 textColor = ImGui::GetColorU32(ImGuiCol_Text); + + drawList->AddRectFilled(ImVec2(origin.x - rulerSize.x, origin.y - rulerSize.y), ImVec2(origin.x + displaySize.x, origin.y), backColor); + drawList->AddRectFilled(ImVec2(origin.x - rulerSize.x, origin.y), ImVec2(origin.x, origin.y + displaySize.y), backColor); + + // steps in preview pixels are chosen to keep ticks and labels apart on screen + static const int s_rulerSteps[] = { 10, 50, 100, 200, 500, 1000 }; + const auto pickStep = [zoom](float minScreenDistance) { + for (const int step : s_rulerSteps) + { + if (step * zoom >= minScreenDistance) + return step; + } + return s_rulerSteps[elementsOf(s_rulerSteps) - 1]; + }; + + const int tickStep = pickStep(5.0f); + const int labelStep = max(pickStep(ImGui::CalcTextSize("0000").x + 8.0f), tickStep); + + const auto tickLength = [&](int pos, float rulerLength) { + if (pos % labelStep == 0) + return rulerLength; + return pos % (tickStep * 5) == 0 ? rulerLength * 0.5f : rulerLength * 0.25f; + }; + + for (int x = 0; x <= size.x; x += tickStep) + { + const float tickX = origin.x + floorf(x * zoom); + drawList->AddLine(ImVec2(tickX, origin.y), ImVec2(tickX, origin.y - tickLength(x, rulerSize.y)), tickColor); + if (x % labelStep == 0) + drawList->AddText(ImVec2(tickX + 2.0f, origin.y - rulerSize.y), textColor, EqString::Format("%d", x)); + } + + for (int y = 0; y <= size.y; y += tickStep) + { + const float tickY = origin.y + floorf(y * zoom); + drawList->AddLine(ImVec2(origin.x, tickY), ImVec2(origin.x - tickLength(y, rulerSize.x), tickY), tickColor); + if (y % labelStep == 0) + drawList->AddText(ImVec2(origin.x - rulerSize.x + 2.0f, tickY + 1.0f), textColor, EqString::Format("%d", y)); + } + + // mouse position marks + const ImVec2 mousePos = ImGui::GetMousePos(); + if (mousePos.x >= origin.x && mousePos.x <= origin.x + displaySize.x) + drawList->AddLine(ImVec2(mousePos.x, origin.y), ImVec2(mousePos.x, origin.y - rulerSize.y), s_selectionColor); + if (mousePos.y >= origin.y && mousePos.y <= origin.y + displaySize.y) + drawList->AddLine(ImVec2(origin.x, mousePos.y), ImVec2(origin.x - rulerSize.x, mousePos.y), s_selectionColor); +} + +void CUIEditor::DrawPreviewOverlay(const ImVec2& origin, float zoom, IUIControl* hovered) +{ + ImDrawList* drawList = ImGui::GetWindowDrawList(); + + // control transforms are not applied to the rectangles + const auto drawControlRect = [&](const IUIControl* control, ImU32 color, float thickness) { + const IAARectangle rect = control->GetClientRectangle(); + drawList->AddRect( + ImVec2(origin.x + floorf(rect.leftTop.x * zoom), origin.y + floorf(rect.leftTop.y * zoom)), + ImVec2(origin.x + floorf(rect.rightBottom.x * zoom), origin.y + floorf(rect.rightBottom.y * zoom)), + color, 0.0f, 0, thickness); + }; + + if (m_showBounds) + { + const EqFunction drawBounds = [&](IUIControl* control) { + if (!control->IsVisible()) + return true; + drawControlRect(control, s_boundsColor, 1.0f); + control->ForEachChild(drawBounds); + return true; + }; + m_panel->ForEachChild(drawBounds); + } + + if (hovered) + drawControlRect(hovered, s_hoverColor, 1.0f); + + const IUIControl* selected = GetSelectedControl(); + if (!selected) + return; + + drawControlRect(selected, s_selectionColor, 2.0f); + + // layout values as written in the file, above the rectangle or below it near the top edge + const IVector2D position = selected->GetPosition(); + const IVector2D size = selected->GetSize(); + const EqString label = EqString::Format("pos X: %d Y: %d size W: %d H: %d", position.x, position.y, size.x, size.y); + + const IAARectangle rect = selected->GetClientRectangle(); + const ImVec2 textSize = ImGui::CalcTextSize(label); + const float padding = 2.0f; + + ImVec2 labelPos(origin.x + floorf(rect.leftTop.x * zoom), origin.y + floorf(rect.leftTop.y * zoom) - textSize.y - padding * 2.0f); + if (labelPos.y < ImGui::GetWindowPos().y) + labelPos.y = origin.y + floorf(rect.rightBottom.y * zoom) + padding * 2.0f; + + drawList->AddRectFilled(ImVec2(labelPos.x - padding, labelPos.y - padding), ImVec2(labelPos.x + textSize.x + padding, labelPos.y + textSize.y + padding), IM_COL32(0, 0, 0, 200)); + drawList->AddText(labelPos, s_selectionColor, label); +} + +void CUIEditor::DrawKeyValuesTab() +{ + LayoutDoc* kvDoc = GetKVDoc(); + + // switching the file discards text changes + ImGui::SetNextItemWidth(250.0f); + if (ImGui::BeginCombo("File", kvDoc == m_mainDoc.Ptr() ? "(layout)" : kvDoc->filePath.ToCString())) + { + if (ImGui::Selectable("(layout)", kvDoc == m_mainDoc.Ptr())) + m_kvDocIdx = -1; + + for (int i = 0; i < m_includeDocs.numElem(); ++i) + { + if (!m_includeDocs[i]->valid) + continue; + + if (ImGui::Selectable(m_includeDocs[i]->filePath, kvDoc == m_includeDocs[i].Ptr())) + m_kvDocIdx = i; + } + ImGui::EndCombo(); + + if (GetKVDoc() != kvDoc) + { + m_kvTextEdited = false; + m_kvTextDirty = true; + UpdateKVText(); + kvDoc = GetKVDoc(); + } + } + + ImGui::SameLine(); + ImGui::BeginDisabled(!m_kvTextEdited); + if (ImGui::Button("Apply")) + { + KVSection document; + if (KeyValues::ParseText(m_kvText.ptr(), CString::Length(m_kvText.ptr()), document, "EqUI editor")) + { + kvDoc->root.Clear(); + document.CopyTo(kvDoc->root); + m_kvTextEdited = false; + OnDocumentChanged(kvDoc); + } + else + { + MsgError("EqUI editor: can't parse layout text\n"); + } + } + + ImGui::SameLine(); + if (ImGui::Button("Revert")) + { + m_kvTextEdited = false; + m_kvTextDirty = true; + } + ImGui::EndDisabled(); + + ImGui::SameLine(); + if (m_kvTextEdited && m_kvTextDirty) + ImGui::TextColored(s_editorOnlyTextColor, "Layout was changed in the editor, Apply overwrites the changes"); + else + ImGui::TextDisabled("Sections with '%s' are removed on save", EDITOR_ONLY_KEY); + + if (m_kvText.numElem() == 0) + m_kvText.append(0); + + constexpr ImGuiInputTextFlags textFlags = ImGuiInputTextFlags_AllowTabInput | ImGuiInputTextFlags_CallbackResize; + if (ImGui::InputTextMultiline("##kvText", m_kvText.ptr(), m_kvText.numElem(), ImVec2(-FLT_MIN, -FLT_MIN), textFlags, KVTextResizeCb, &m_kvText)) + m_kvTextEdited = true; +} + +//------------------------------------------------------------------------------------------------- + +void CUIEditor::DrawInspector() +{ + ImGui::TextUnformatted("Inspector"); + ImGui::Separator(); + + IUIControl* control = GetSelectedControl(); + if (!control) + { + ImGui::TextDisabled("Select a control in the outliner or the preview"); + return; + } + + const SectionRef ref = FindSectionByPath(m_selection); + KVSection* sec = ref.sec; + const bool isPanel = control == m_panel; + + ImGui::Text("Class: %s", control->GetClassname()); + + const IAARectangle rect = control->GetClientRectangle(); + ImGui::TextDisabled("Screen rect: %d %d - %d %d", rect.leftTop.x, rect.leftTop.y, rect.rightBottom.x, rect.rightBottom.y); + + if (!sec) + { + ImGui::Text("Name: %s", control->GetName()); + ImGui::TextWrapped("Control is created by its parent and has no layout section."); + return; + } + + if (ref.doc != m_mainDoc.Ptr()) + { + ImGui::TextColored(s_includeTextColor, "File: %s%s", ref.doc->filePath.ToCString(), ref.doc->modified ? "*" : ""); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Control is written in the included file, the file is saved with the layout"); + } + + if (ref.editorOnlyInclude) + ImGui::TextColored(s_editorOnlyTextColor, "Included as editor-only, removed from the saved layout"); + + if (!isPanel) + { + char nameBuffer[128]; + strncpy(nameBuffer, control->GetName(), sizeof(nameBuffer) - 1); + nameBuffer[sizeof(nameBuffer) - 1] = 0; + + if (ImGui::InputText("Name", nameBuffer, sizeof(nameBuffer), ImGuiInputTextFlags_EnterReturnsTrue) && nameBuffer[0]) + { + sec->SetValue(nameBuffer, 1); + + Select(JoinPath(GetParentPath(m_selection), nameBuffer)); + OnDocumentChanged(ref.doc); + } + + bool editorOnly = IsEditorOnly(*sec); + if (ImGui::Checkbox("Editor only", &editorOnly)) + { + if (editorOnly) + sec->SetKey(EDITOR_ONLY_KEY, 1); + else + sec->RemoveSectionByName(EDITOR_ONLY_KEY); + OnDocumentChanged(ref.doc); + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Shown in the preview only, removed from the saved layout"); + } + + ImGui::Separator(); + + // class is the first value of child sections + const char* className = isPanel ? "panel" : sec->ValueAt(0).GetString(); + + static Array localFonts{ PP_SL }; + localFonts.clear(); + + CollectLocalFonts(m_selection, localFonts); + + ImGui::InputTextWithHint("##filter", "Filter", m_propertyFilter, sizeof(m_propertyFilter)); + const PropertiesContext propsCtx{ localFonts, m_propertyFilter }; + + if (DrawControlProperties(*sec, className, propsCtx)) + OnDocumentChanged(ref.doc); + + DrawIncludes(ref); +} + +// 'child file' entries of the control section +void CUIEditor::DrawIncludes(const SectionRef& ref) +{ + ImGui::Separator(); + ImGui::TextUnformatted("Included files"); + + KVSection& sec = *ref.sec; + int removeKeyIdx = -1; + + for (int i = 0; i < sec.KeyCount(); ++i) + { + KVSection& childSec = sec.KeyAt(i); + if (CString::CompareCaseIns(childSec.GetName(), "child") || !IsIncludeSection(childSec)) + continue; + + ImGui::PushID(i); + if (ImGui::SmallButton("x")) + removeKeyIdx = i; + + ImGui::SameLine(); + const char* includePath = GetChildSectionName(childSec); + const int includeIdx = arrayFindIndexF(m_includeDocs, [&](const LayoutDocPtr& doc) { + return !doc->filePath.CompareCaseIns(includePath); + }); + + bool includeEditorOnly = IsEditorOnly(childSec); + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 0.0f)); + if (ImGui::Checkbox("##editorOnly", &includeEditorOnly)) + { + if (includeEditorOnly) + childSec.SetKey(EDITOR_ONLY_KEY, 1); + else + childSec.RemoveSectionByName(EDITOR_ONLY_KEY); + OnDocumentChanged(ref.doc); + } + ImGui::PopStyleVar(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Editor only: shown in the preview, removed on save"); + + ImGui::SameLine(); + ImGui::BeginDisabled(includeIdx == -1 || !m_includeDocs[includeIdx]->valid); + if (ImGui::SmallButton("KV")) + { + m_kvDocIdx = includeIdx; + m_kvTextEdited = false; + m_kvTextDirty = true; + m_selectKVTab = true; + } + ImGui::EndDisabled(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Show the file in the KeyValues tab"); + + ImGui::SameLine(); + ImGui::BeginDisabled(includeIdx == -1); + if (ImGui::SmallButton("R")) + ReloadIncludeDoc(*m_includeDocs[includeIdx]); + ImGui::EndDisabled(); + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Reload the file from disk, its unsaved changes are lost"); + + ImGui::SameLine(); + char pathBuffer[256]; + strncpy(pathBuffer, includePath, sizeof(pathBuffer) - 1); + pathBuffer[sizeof(pathBuffer) - 1] = 0; + + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputText("##includePath", pathBuffer, sizeof(pathBuffer), ImGuiInputTextFlags_EnterReturnsTrue) && pathBuffer[0]) + { + childSec.SetValue(pathBuffer, 1); + OnDocumentChanged(ref.doc); + } + ImGui::PopID(); + } + + if (removeKeyIdx != -1) + { + sec.RemoveSection(&sec.KeyAt(removeKeyIdx)); + OnDocumentChanged(ref.doc); + } + + char newPath[256]{}; + ImGui::SetNextItemWidth(-FLT_MIN); + if (ImGui::InputTextWithHint("##addInclude", "Add file, e.g. resources/ui_partial/menu_fonts.res", newPath, sizeof(newPath), ImGuiInputTextFlags_EnterReturnsTrue) && newPath[0]) + AddIncludeSection(m_selection, newPath, false); +} + +} + +#endif // EQUI_EDITOR + +//------------------------------------------------------------------------------------------------- + +void equiEditorUIDraw(bool& open) +{ +#ifdef EQUI_EDITOR + equi::Editor::s_uiEditor->Draw(open); +#endif +} + +bool equiEditorOpenFile(const char* filePath) +{ +#ifdef EQUI_EDITOR + return equi::Editor::s_uiEditor->LoadDocument(filePath); +#else + return false; +#endif +} + +void equiEditorShutdown() +{ +#ifdef EQUI_EDITOR + equi::Editor::s_uiEditor->Shutdown(); +#endif +} diff --git a/shared/equi/EqUI_Editor.h b/shared/equi/EqUI_Editor.h new file mode 100644 index 000000000..b679b0294 --- /dev/null +++ b/shared/equi/EqUI_Editor.h @@ -0,0 +1,21 @@ +////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) Inspiration Byte +// 2009-2025 +////////////////////////////////////////////////////////////////////////////////// +// Description: EqUI editor +////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +#if !defined(_RETAIL) && !defined(_PROFILE) +#define EQUI_EDITOR +#endif + +// draws the editor window, used as an ImGui debug menu handler +void equiEditorUIDraw(bool& open); + +// opens a layout file in the editor +bool equiEditorOpenFile(const char* filePath); + +// destroys preview controls and targets, must be called before equi::Manager->Shutdown() +void equiEditorShutdown(); diff --git a/shared/equi/EqUI_EditorProps.cpp b/shared/equi/EqUI_EditorProps.cpp new file mode 100644 index 000000000..0a8f016f2 --- /dev/null +++ b/shared/equi/EqUI_EditorProps.cpp @@ -0,0 +1,793 @@ +////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) Inspiration Byte +// 2009-2025 +////////////////////////////////////////////////////////////////////////////////// +// Description: EqUI editor - control property inspector +////////////////////////////////////////////////////////////////////////////////// + +#include "core/core_common.h" +#include "utils/KeyValues.h" + +#include "EqUI_Editor.h" +#include "EqUI_EditorProps.h" + +#ifdef EQUI_EDITOR +#include + +namespace equi::Editor +{ + +enum EPropType +{ + PROP_BOOL, + PROP_INT2, + PROP_FLOAT, + PROP_FLOAT2, + PROP_COLOR, + PROP_STRING, + PROP_VALUES, // raw list of values, e.g. font "Roboto" 20 bold + PROP_FLAGS, // any combination of option tokens + PROP_ENUM, // one of option tokens + PROP_FONT, // local font name or font description values + PROP_TRANSFORM, // nested section: rotate, translate, elementScale +}; + +struct PropDesc +{ + const char* key; + EPropType type; + const char* className; // nullptr - property of every control + const char* const* options; + int numOptions; + float defValue[4]; // shown while key is not set +}; + +static const char* s_alignOptions[] = { "left", "hcenter", "right", "top", "vcenter", "bottom" }; +static const char* s_anchorOptions[] = { "left", "top", "right", "bottom" }; +static const char* s_scalingOptions[] = { "none", "inherit", "inherit_min", "inherit_max", "aspectw", "aspecth", "uniform", "aspect_min", "aspect_max" }; + +#define PROP_OPTIONS(arr) arr, elementsOf(arr) + +// keys parsed by IUIControl::Parse and the control classes, see equi_overview.md +static const PropDesc s_propDescs[] = { + { "position", PROP_INT2, nullptr, nullptr, 0, { 0, 0 } }, + { "size", PROP_INT2, nullptr, nullptr, 0, { 64, 64 } }, + { "visible", PROP_BOOL, nullptr, nullptr, 0, { 1 } }, + { "selfVisible", PROP_BOOL, nullptr, nullptr, 0, { 1 } }, + { "label", PROP_STRING, nullptr, nullptr, 0, {} }, + { "align", PROP_FLAGS, nullptr, PROP_OPTIONS(s_alignOptions), {} }, + { "anchors", PROP_FLAGS, nullptr, PROP_OPTIONS(s_anchorOptions), {} }, + { "scaling", PROP_ENUM, nullptr, PROP_OPTIONS(s_scalingOptions), {} }, + { "transform", PROP_TRANSFORM, nullptr, nullptr, 0, {} }, + { "clipChilds", PROP_BOOL, nullptr, nullptr, 0, { 1 } }, + { "clipTransform", PROP_BOOL, nullptr, nullptr, 0, { 1 } }, + { "command", PROP_VALUES, nullptr, nullptr, 0, {} }, + { "font", PROP_FONT, nullptr, nullptr, 0, {} }, + { "fontScale", PROP_FLOAT2, nullptr, nullptr, 0, { 1, 1 } }, + { "textColor", PROP_COLOR, nullptr, nullptr, 0, { 1, 1, 1, 1 } }, + { "textAlign", PROP_FLAGS, nullptr, PROP_OPTIONS(s_alignOptions), {} }, + + { "color", PROP_COLOR, "panel", nullptr, 0, { 1, 1, 1, 1 } }, + { "window", PROP_BOOL, "panel", nullptr, 0, { 0 } }, + { "screenoverlay", PROP_BOOL, "panel", nullptr, 0, { 0 } }, + + { "file", PROP_STRING, "container", nullptr, 0, {} }, + + { "path", PROP_STRING, "image", nullptr, 0, {} }, + { "atlas", PROP_VALUES, "image", nullptr, 0, {} }, + { "uvLeftTop", PROP_FLOAT2, "image", nullptr, 0, { 0, 0 } }, + { "uvRightBottom", PROP_FLOAT2, "image", nullptr, 0, { 1, 1 } }, + { "color", PROP_COLOR, "image", nullptr, 0, { 1, 1, 1, 1 } }, + { "flipX", PROP_BOOL, "image", nullptr, 0, { 0 } }, + { "flipY", PROP_BOOL, "image", nullptr, 0, { 0 } }, + + { "image", PROP_STRING, "mask", nullptr, 0, {} }, + { "color", PROP_COLOR, "mask", nullptr, 0, { 1, 1, 1, 1 } }, + + { "value", PROP_FLOAT, "progressBar", nullptr, 0, { 0 } }, + { "color", PROP_COLOR, "progressBar", nullptr, 0, { 1, 1, 1, 1 } }, +}; + +static constexpr const ImU32 s_unsetKeyColor = IM_COL32(128, 128, 128, 255); + +//------------------------------------------------------------------------------------------------- + +static bool IsPropOfClass(const PropDesc& desc, const char* className) +{ + return !desc.className || !CString::CompareCaseIns(desc.className, className); +} + +static bool IsKnownKey(const char* key, const char* className) +{ + for (const PropDesc& desc : s_propDescs) + { + if (IsPropOfClass(desc, className) && !CString::CompareCaseIns(desc.key, key)) + return true; + } + return false; +} + +// creates the key or removes its values +static KVSection& ResetKey(KVSection& sec, KVSection*& key, const char* name) +{ + if (!key) + key = &sec.CreateSection(name); + key->ClearValues(); + return *key; +} + +static bool NeedsQuotes(const char* str) +{ + if (!*str) + return true; + + for (const char* c = str; *c; ++c) + { + if (CType::IsSpace(*c) || strchr("{};\"", *c)) + return true; + } + return false; +} + +static EqString ValuesToText(const KVSection* key) +{ + EqString text; + if (!key) + return text; + + for (int i = 0; i < key->ValueCount(); ++i) + { + const char* value = key->ValueAt(i).GetString(); + if (i > 0) + text.Append(' '); + + if (NeedsQuotes(value)) + text.Append(EqString::Format("\"%s\"", value)); + else + text.Append(value); + } + return text; +} + +// values text is parsed with the KeyValues parser, so quoting rules are the same as in .res files +static bool TextToValues(KVSection& key, const char* text) +{ + const EqString keyText = EqString::Format("v %s;", text); + + KVSection parsed; + if (!KeyValues::ParseText(keyText, keyText.Length(), parsed)) + return false; + + const KVSection* parsedKey = parsed.FindSection("v"); + if (!parsedKey) + return false; + + key.ClearValues(); + for (int i = 0; i < parsedKey->ValueCount(); ++i) + key.AddValue(parsedKey->ValueAt(i)); + + return true; +} + +static bool HasOption(const KVSection* key, const char* option) +{ + if (!key) + return false; + + for (EqStringRef value : key->Values()) + { + if (!value.CompareCaseIns(option)) + return true; + + // shortcuts accepted by the parser + if (!value.CompareCaseIns("center") && (!CString::CompareCaseIns(option, "hcenter") || !CString::CompareCaseIns(option, "vcenter"))) + return true; + if (!value.CompareCaseIns("all")) + return true; + } + return false; +} + +static void SetFloatValues(KVSection& sec, const char* name, const float* values, int count) +{ + KVSection* key = sec.FindSection(name); + KVSection& newKey = ResetKey(sec, key, name); + for (int i = 0; i < count; ++i) + newKey.AddValue(values[i]); +} + +// replaces values of the key with parsed text, the key is created if it is valid +static bool ApplyValuesText(KVSection& sec, KVSection*& key, const char* name, const char* text) +{ + KVSection* newKey = key ? key : &sec.CreateSection(name); + if (!TextToValues(*newKey, text)) + { + MsgError("EqUI editor: can't parse values of '%s'\n", name); + if (!key) + sec.RemoveSection(newKey); + return false; + } + + key = newKey; + return true; +} + +//------------------------------------------------------------------------------------------------- + +static bool DrawTextInput(const char* id, const EqString& value, EqString& outValue) +{ + char buffer[512]; + strncpy(buffer, value.ToCString(), sizeof(buffer) - 1); + buffer[sizeof(buffer) - 1] = 0; + + ImGui::SetNextItemWidth(-FLT_MIN); + if (!ImGui::InputText(id, buffer, sizeof(buffer), ImGuiInputTextFlags_EnterReturnsTrue)) + { + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("Press Enter to apply"); + return false; + } + + outValue = buffer; + return true; +} + +// edits value of the key, creates it on change +static bool DrawPropertyValue(KVSection& sec, KVSection*& key, const PropDesc& desc, const PropertiesContext& ctx) +{ + ImGui::SetNextItemWidth(-FLT_MIN); + + switch (desc.type) + { + case PROP_BOOL: + { + bool value = KV_GetValueBool(key, 0, desc.defValue[0] > 0.0f); + if (!ImGui::Checkbox("##value", &value)) + return false; + + ResetKey(sec, key, desc.key).AddValue(value ? 1 : 0); + return true; + } + case PROP_INT2: + { + IVector2D value(desc.defValue[0], desc.defValue[1]); + if (key) + key->GetValues(value); + + if (!ImGui::DragInt2("##value", &value.x)) + return false; + + KVSection& newKey = ResetKey(sec, key, desc.key); + newKey.AddValue(value.x); + newKey.AddValue(value.y); + return true; + } + case PROP_FLOAT: + { + float value = KV_GetValueFloat(key, 0, desc.defValue[0]); + if (!ImGui::DragFloat("##value", &value, 0.01f)) + return false; + + ResetKey(sec, key, desc.key).AddValue(value); + return true; + } + case PROP_FLOAT2: + { + Vector2D value = KV_GetVector2D(key, 0, Vector2D(desc.defValue[0], desc.defValue[1])); + if (!ImGui::DragFloat2("##value", &value.x, 0.01f)) + return false; + + KVSection& newKey = ResetKey(sec, key, desc.key); + newKey.AddValue(value.x); + newKey.AddValue(value.y); + return true; + } + case PROP_COLOR: + { + Vector4D value = KV_GetVector4D(key, 0, Vector4D(desc.defValue[0], desc.defValue[1], desc.defValue[2], desc.defValue[3])); + if (!ImGui::ColorEdit4("##value", &value.x, ImGuiColorEditFlags_Float)) + return false; + + KVSection& newKey = ResetKey(sec, key, desc.key); + for (int i = 0; i < 4; ++i) + newKey.AddValue(value[i]); + return true; + } + case PROP_STRING: + { + EqString value; + if (!DrawTextInput("##value", KV_GetValueString(key, 0, ""), value)) + return false; + + ResetKey(sec, key, desc.key).AddValue(value.ToCString()); + return true; + } + case PROP_VALUES: + { + EqString value; + if (!DrawTextInput("##value", ValuesToText(key), value)) + return false; + + return ApplyValuesText(sec, key, desc.key, value); + } + case PROP_FONT: + { + // single value is a name from 'fonts', several values describe the font itself + bool changed = false; + const EqString current = ValuesToText(key); + + if (ImGui::BeginCombo("##localFont", current.Length() ? current.ToCString() : "(not set)")) + { + if (ctx.localFonts.numElem() == 0) + ImGui::TextDisabled("No 'fonts' in the layout"); + + for (const EqString& fontName : ctx.localFonts) + { + const bool selected = key && key->ValueCount() == 1 && !fontName.CompareCaseIns(key->ValueAt(0).GetString()); + if (ImGui::Selectable(fontName, selected) && !selected) + { + ResetKey(sec, key, desc.key).AddValue(fontName.ToCString()); + changed = true; + } + } + ImGui::EndCombo(); + } + + EqString value; + if (DrawTextInput("##value", current, value)) + changed = ApplyValuesText(sec, key, desc.key, value) || changed; + + return changed; + } + case PROP_TRANSFORM: + { + // same keys and defaults as in IUIControl::Parse + float rotate = KV_GetValueFloat(key ? key->FindSection("rotate") : nullptr, 0, 0.0f); + Vector2D translate = KV_GetVector2D(key ? key->FindSection("translate") : nullptr, 0, vec2_zero); + Vector2D elementScale = KV_GetVector2D(key ? key->FindSection("elementScale") : nullptr, 0, Vector2D(1.0f)); + + // short labels to fit narrow columns and menus, key names are in the tooltips + const auto beginRow = [&](const char* label, const char* tooltip) { + ImGui::SetNextItemWidth(40); + ImGui::TextUnformatted(label); + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", tooltip); + ImGui::SameLine(45); + ImGui::SetNextItemWidth(-FLT_MIN); + }; + + beginRow("T", "translate"); + const bool translateChanged = ImGui::DragFloat2("##translate", &translate.x, 0.5f); + + beginRow("R", "rotate, degrees"); + const bool rotateChanged = ImGui::DragFloat("##rotate", &rotate, 0.5f, 0.0f, 0.0f, "%.1f"); + + beginRow("S", "elementScale"); + const bool scaleChanged = ImGui::DragFloat2("##elementScale", &elementScale.x, 0.01f); + + if (!rotateChanged && !translateChanged && !scaleChanged) + return false; + + // nested keys are kept, only the changed one is written + if (!key) + key = &sec.CreateSection(desc.key); + + if (rotateChanged) + SetFloatValues(*key, "rotate", &rotate, 1); + if (translateChanged) + SetFloatValues(*key, "translate", &translate.x, 2); + if (scaleChanged) + SetFloatValues(*key, "elementScale", &elementScale.x, 2); + + return true; + } + case PROP_FLAGS: + { + const ImGuiStyle& style = ImGui::GetStyle(); + const float maxX = ImGui::GetCursorScreenPos().x + ImGui::GetContentRegionAvail().x; + + bool changed = false; + uint optionBits = 0; + for (int i = 0; i < desc.numOptions; ++i) + { + bool hasOption = HasOption(key, desc.options[i]); + + // options that don't fit go to the next line + const float optionWidth = ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize(desc.options[i]).x; + if (i > 0 && ImGui::GetItemRectMax().x + style.ItemSpacing.x + optionWidth <= maxX) + ImGui::SameLine(); + + ImGui::PushID(i); + changed = ImGui::Checkbox(desc.options[i], &hasOption) || changed; + ImGui::PopID(); + + if (hasOption) + optionBits |= (1 << i); + } + + if (!changed) + return false; + + KVSection& newKey = ResetKey(sec, key, desc.key); + for (int i = 0; i < desc.numOptions; ++i) + { + if (optionBits & (1 << i)) + newKey.AddValue(desc.options[i]); + } + return true; + } + case PROP_ENUM: + { + const char* current = KV_GetValueString(key, 0, desc.options[0]); + if (!ImGui::BeginCombo("##value", current)) + return false; + + bool changed = false; + for (int i = 0; i < desc.numOptions; ++i) + { + const bool selected = !CString::CompareCaseIns(current, desc.options[i]); + if (ImGui::Selectable(desc.options[i], selected) && !selected) + { + ResetKey(sec, key, desc.key).AddValue(desc.options[i]); + changed = true; + } + } + ImGui::EndCombo(); + return changed; + } + } + return false; +} + +static void SetDefaultValue(KVSection& key, const PropDesc& desc, const PropertiesContext& ctx) +{ + switch (desc.type) + { + case PROP_BOOL: + key.AddValue(desc.defValue[0] > 0.0f ? 1 : 0); + break; + case PROP_INT2: + key.AddValue(static_cast(desc.defValue[0])); + key.AddValue(static_cast(desc.defValue[1])); + break; + case PROP_FLOAT: + key.AddValue(desc.defValue[0]); + break; + case PROP_FLOAT2: + key.AddValue(desc.defValue[0]); + key.AddValue(desc.defValue[1]); + break; + case PROP_COLOR: + for (int i = 0; i < 4; ++i) + key.AddValue(desc.defValue[i]); + break; + case PROP_STRING: + key.AddValue(""); + break; + case PROP_ENUM: + key.AddValue(desc.options[0]); + break; + case PROP_FONT: + key.AddValue(ctx.localFonts.numElem() ? ctx.localFonts[0].ToCString() : "default"); + break; + case PROP_TRANSFORM: + { + const float rotate = 0.0f; + const float translate[] = { 0.0f, 0.0f }; + const float elementScale[] = { 1.0f, 1.0f }; + SetFloatValues(key, "rotate", &rotate, 1); + SetFloatValues(key, "translate", translate, 2); + SetFloatValues(key, "elementScale", elementScale, 2); + break; + } + default: + break; + } +} + +// checkbox in the first column tells if the key is written to the layout +static bool DrawProperty(KVSection& sec, const PropDesc& desc, const PropertiesContext& ctx) +{ + KVSection* key = sec.FindSection(desc.key); + bool changed = false; + + ImGui::PushID(desc.key); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + bool isSet = key != nullptr; + if (ImGui::Checkbox("##set", &isSet)) + { + if (isSet) + { + key = &sec.CreateSection(desc.key); + SetDefaultValue(*key, desc, ctx); + } + else + { + sec.RemoveSection(key); + key = nullptr; + } + changed = true; + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip(isSet ? "Remove key from the layout" : "Write key to the layout"); + + ImGui::SameLine(); + if (!key) + ImGui::PushStyleColor(ImGuiCol_Text, s_unsetKeyColor); + ImGui::TextUnformatted(desc.key); + if (!key) + ImGui::PopStyleColor(); + + ImGui::TableNextColumn(); + changed = DrawPropertyValue(sec, key, desc, ctx) || changed; + + ImGui::PopID(); + return changed; +} + +// keys that the inspector does not know are edited as raw values +static bool DrawOtherKeys(const PropertiesContext& ctx, KVSection& sec, const char* className) +{ + bool changed = false; + int removeKeyIdx = -1; + + for (int i = 0; i < sec.KeyCount(); ++i) + { + KVSection& key = sec.KeyAt(i); + if (!CString::CompareCaseIns(key.GetName(), "child") || !CString::CompareCaseIns(key.GetName(), EDITOR_ONLY_KEY)) + continue; + + if (IsKnownKey(key.GetName(), className)) + continue; + + if (ctx.filter.Length() > 0 && _Es(key.GetName()).Find(ctx.filter, false, 0) != -1) + continue; + + ImGui::PushID(i); + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + + if (ImGui::SmallButton("x")) + removeKeyIdx = i; + + ImGui::SameLine(); + ImGui::TextUnformatted(key.GetName()); + ImGui::TableNextColumn(); + + if (key.KeyCount() > 0) + { + // nested sections like transform and fonts + ImGui::TextDisabled("{ %d keys } - edit in KeyValues tab", key.KeyCount()); + } + else + { + EqString value; + if (DrawTextInput("##value", ValuesToText(&key), value)) + { + if (TextToValues(key, value)) + changed = true; + else + MsgError("EqUI editor: can't parse values of '%s'\n", key.GetName()); + } + } + ImGui::PopID(); + } + + if (removeKeyIdx != -1) + { + sec.RemoveSection(&sec.KeyAt(removeKeyIdx)); + changed = true; + } + + return changed; +} + +// adds keys written as in .res file: key value1 value2 +static bool DrawAddKey(KVSection& sec) +{ + EqString text; + ImGui::TextUnformatted("Add key"); + if (!DrawTextInput("##addKey", EqString(), text) || text.Length() == 0) + return false; + + const EqString keysText = EqString::Format("%s;", text.ToCString()); + + KVSection parsed; + if (!KeyValues::ParseText(keysText, keysText.Length(), parsed)) + { + MsgError("EqUI editor: can't parse key '%s'\n", text.ToCString()); + return false; + } + + for (const KVSection* parsedKey : parsed.keys) + { + KVSection* key = sec.FindSection(parsedKey->GetName()); + if (!key) + key = &sec.CreateSection(parsedKey->GetName()); + parsedKey->CopyTo(*key); + } + return parsed.KeyCount() > 0; +} + +static const PropDesc& GetCommonPropDesc(const char* key) +{ + for (const PropDesc& desc : s_propDescs) + { + if (!desc.className && !CString::CompareCaseIns(desc.key, key)) + return desc; + } + + ASSERT_FAIL("unknown property %s", key); + return s_propDescs[0]; +} + +// 3x3 grid, missing key is left top like in IUIControl +static bool DrawAlignmentGrid(KVSection& sec) +{ + static const char* s_hAlign[] = { "left", "hcenter", "right" }; + static const char* s_vAlign[] = { "top", "vcenter", "bottom" }; + + KVSection* key = sec.FindSection("align"); + + int curH = 0; + int curV = 0; + if (key) + { + curH = HasOption(key, "hcenter") ? 1 : (HasOption(key, "right") ? 2 : 0); + curV = HasOption(key, "vcenter") ? 1 : (HasOption(key, "bottom") ? 2 : 0); + } + + bool changed = false; + for (int v = 0; v < 3; ++v) + { + for (int h = 0; h < 3; ++h) + { + if (h > 0) + ImGui::SameLine(); + + ImGui::PushID(v * 3 + h); + if (ImGui::RadioButton("##align", h == curH && v == curV) && !(h == curH && v == curV)) + { + KVSection& newKey = ResetKey(sec, key, "align"); + newKey.AddValue(s_hAlign[h]); + newKey.AddValue(s_vAlign[v]); + changed = true; + } + + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s %s", s_hAlign[h], s_vAlign[v]); + ImGui::PopID(); + } + } + return changed; +} + +// checkboxes placed on the sides of a square +static bool DrawAnchorsCross(KVSection& sec) +{ + KVSection* key = sec.FindSection("anchors"); + + bool sides[4]; + for (int i = 0; i < 4; ++i) + sides[i] = HasOption(key, s_anchorOptions[i]); + + const float boxSize = ImGui::GetFrameHeight(); + const float spacing = ImGui::GetStyle().ItemSpacing.x; + const float startX = ImGui::GetCursorPosX(); + + // left, top, right, bottom + bool changed = false; + const auto drawSide = [&](int side, float offsetX) { + ImGui::SetCursorPosX(startX + offsetX); + ImGui::PushID(side); + changed = ImGui::Checkbox("##anchor", &sides[side]) || changed; + if (ImGui::IsItemHovered()) + ImGui::SetTooltip("%s", s_anchorOptions[side]); + ImGui::PopID(); + }; + + drawSide(1, boxSize + spacing); + drawSide(0, 0.0f); + ImGui::SameLine(); + drawSide(2, (boxSize + spacing) * 2.0f); + drawSide(3, boxSize + spacing); + + if (!changed) + return false; + + KVSection& newKey = ResetKey(sec, key, "anchors"); + for (int i = 0; i < 4; ++i) + { + if (sides[i]) + newKey.AddValue(s_anchorOptions[i]); + } + return true; +} + +//------------------------------------------------------------------------------------------------- + +bool IsEditorOnly(const KVSection& sec) +{ + return KV_GetValueBool(sec.FindSection(EDITOR_ONLY_KEY), 0, false); +} +PRAGMA_OPTIMIZE_OFF +bool DrawControlProperties(KVSection& sec, const char* className, const PropertiesContext& ctx) +{ + bool changed = false; + + constexpr ImGuiTableFlags tableFlags = ImGuiTableFlags_BordersInnerV | ImGuiTableFlags_RowBg | ImGuiTableFlags_Resizable; + if (ImGui::BeginTable("##properties", 2, tableFlags)) + { + ImGui::TableSetupColumn("Key", ImGuiTableColumnFlags_WidthFixed, 130.0f); + ImGui::TableSetupColumn("Value", ImGuiTableColumnFlags_WidthStretch); + + for (const PropDesc& desc : s_propDescs) + { + if (!IsPropOfClass(desc, className)) + continue; + + if (ctx.filter.Length() > 0 && _Es(desc.key).Find(ctx.filter, false, 0) == -1) + continue; + + changed = DrawProperty(sec, desc, ctx) || changed; + } + + changed = DrawOtherKeys(ctx, sec, className) || changed; + ImGui::EndTable(); + } + + ImGui::Separator(); + changed = DrawAddKey(sec) || changed; + + return changed; +} + +bool DrawLayoutProperties(KVSection& sec) +{ + static const Array s_noFonts{ PP_SL }; + const PropertiesContext ctx{ s_noFonts }; + + bool changed = false; + + constexpr ImGuiTableFlags pickerFlags = ImGuiTableFlags_SizingFixedFit; + if (ImGui::BeginTable("##layoutPickers", 2, pickerFlags)) + { + ImGui::TableNextRow(); + ImGui::TableNextColumn(); + ImGui::TextUnformatted("Align"); + changed = DrawAlignmentGrid(sec) || changed; + + ImGui::TableNextColumn(); + ImGui::TextUnformatted("Anchors"); + changed = DrawAnchorsCross(sec) || changed; + + ImGui::EndTable(); + } + + // popups and menus are sized by their content, values take the remaining width + constexpr float labelWidth = 70.0f; + constexpr float valueWidth = 180.0f; + for (const char* keyName : { "position", "size", "scaling", "transform" }) + { + const PropDesc& desc = GetCommonPropDesc(keyName); + KVSection* key = sec.FindSection(desc.key); + + ImGui::PushID(keyName); + ImGui::TextUnformatted(keyName); + ImGui::SameLine(labelWidth); + ImGui::BeginGroup(); + ImGui::Dummy(ImVec2(valueWidth, 0.0f)); + changed = DrawPropertyValue(sec, key, desc, ctx) || changed; + ImGui::EndGroup(); + ImGui::PopID(); + } + + return changed; +} + +} + +#endif // EQUI_EDITOR diff --git a/shared/equi/EqUI_EditorProps.h b/shared/equi/EqUI_EditorProps.h new file mode 100644 index 000000000..eb0717145 --- /dev/null +++ b/shared/equi/EqUI_EditorProps.h @@ -0,0 +1,30 @@ +////////////////////////////////////////////////////////////////////////////////// +// Copyright (C) Inspiration Byte +// 2009-2025 +////////////////////////////////////////////////////////////////////////////////// +// Description: EqUI editor - control property inspector +////////////////////////////////////////////////////////////////////////////////// + +#pragma once + +struct KVSection; + +namespace equi::Editor +{ +// key of the editor-only child sections, they are shown in the preview and removed on save +static constexpr const char* EDITOR_ONLY_KEY = "editorOnly"; + +struct PropertiesContext +{ + const Array& localFonts; // names from 'fonts' sections of the layout available to the control + EqStringRef filter; +}; + +bool IsEditorOnly(const KVSection& sec); + +// draws property editors of the control layout section, returns true when the section is changed +bool DrawControlProperties(KVSection& sec, const char* className, const PropertiesContext& ctx); + +// alignment and anchors as visual pickers, position, size and scaling; returns true when the section is changed +bool DrawLayoutProperties(KVSection& sec); +} diff --git a/shared/equi/EqUI_Manager.cpp b/shared/equi/EqUI_Manager.cpp index f0ec06199..e05d2a0b1 100644 --- a/shared/equi/EqUI_Manager.cpp +++ b/shared/equi/EqUI_Manager.cpp @@ -8,6 +8,8 @@ #include "core/core_common.h" #include "core/ConCommand.h" #include "core/ConVar.h" +#include "core/IFileSystem.h" +#include "utils/KeyValues.h" #include "EqUI_Manager.h" #include "font/IFontCache.h" @@ -23,7 +25,7 @@ DECLARE_CMD(ui_showpanel, nullptr, CV_CHEAT) if(CMD_ARGC == 0) return; - equi::Panel* pPanel = equi::Manager->FindPanel( CMD_ARGV(0).ToCString() ); + equi::Panel* pPanel = equi::Manager->FindPanel( CMD_ARGV(0) ); if( pPanel ) pPanel->Show(); @@ -36,7 +38,7 @@ DECLARE_CMD(ui_hidepanel, nullptr, CV_CHEAT) if(CMD_ARGC == 0) return; - equi::Panel* pPanel = equi::Manager->FindPanel( CMD_ARGV(0).ToCString() ); + equi::Panel* pPanel = equi::Manager->FindPanel( CMD_ARGV(0) ); if( pPanel ) pPanel->Hide(); @@ -54,10 +56,6 @@ DECLARE_CVAR(equi_debug, "0", nullptr, 0); namespace equi { -CUIManager::CUIManager() : m_keyboardFocus(nullptr), m_rootPanel(nullptr), m_defaultFont(nullptr), m_mousePos(0) -{ -} - CUIManager::~CUIManager() { Shutdown(); @@ -77,6 +75,7 @@ void CUIManager::Init() m_rootPanel = (Panel*)CreateElement("Panel"); m_rootPanel->SetName("equi_root"); + m_rootPanel->SetSelfVisible(false); m_rootPanel->Show(); m_defaultFont = g_fontCache->GetFont("default", 22); @@ -89,13 +88,27 @@ void CUIManager::Shutdown() m_panels.clear(true); } +bool CUIManager::LoadLayoutFile(const char* fileName, KVSection& outSec) const +{ + if (m_layoutFileLoader) + return m_layoutFileLoader(fileName, outSec); + + return KV_LoadFromFile(fileName, SP_MOD, outSec); +} + +void CUIManager::GetControlClassNames(Array& outNames) const +{ + for (const Factory& factory : m_controlFactory) + outNames.append(factory.name); +} + Panel* CUIManager::GetRootPanel() const { return m_rootPanel; } // the element loader -void CUIManager::RegisterFactory(const char* name, ControlFactoryFunc factory) +void CUIManager::RegisterFactory(const char* name, ControlFactoryFn factory) { for(int i = 0; i < m_controlFactory.numElem(); i++) { @@ -106,18 +119,18 @@ void CUIManager::RegisterFactory(const char* name, ControlFactoryFunc factory) } } - ControlFactory& fac = m_controlFactory.append(); + Factory& fac = m_controlFactory.append(); fac.factory = factory; fac.name = name; } // the element loader -IUIControl* CUIManager::CreateElement( const char* type ) +IUIControl* CUIManager::CreateElement( const char* type ) const { - for(int i = 0; i < m_controlFactory.numElem(); i++) + for(const Factory& fac : m_controlFactory) { - if(!CString::CompareCaseIns(m_controlFactory[i].name, type)) - return (*m_controlFactory[i].factory)(); + if(!CString::CompareCaseIns(fac.name, type)) + return (*fac.factory)(); } ASSERT_FAIL("Unknown UI element type '%s'", type); @@ -165,25 +178,24 @@ void CUIManager::DestroyPanel( Panel* panel ) Panel* CUIManager::FindPanel( const char* pszPanelName ) const { - for(int i = 0; i < m_panels.numElem(); i++) - { - if(strlen(m_panels[i]->GetName()) > 0) - { - if(!CString::CompareCaseIns(pszPanelName, m_panels[i]->GetName())) - return m_panels[i]; - } - } + const int idx = arrayFindIndexF(m_panels, [pszPanelName](Panel* panel){ + return panel->m_name.Length() > 0 && !panel->m_name.CompareCaseIns(pszPanelName); + }); - return nullptr; + if (idx == -1) + return nullptr; + + return m_panels[idx]; } -void CUIManager::BringToTop( equi::Panel* panel ) +void CUIManager::BringToTop( Panel* panel ) { auto it = m_rootPanel->m_childs.findFront(panel); - m_rootPanel->m_childs.moveToFront(it); + if(!it.atEnd()) + m_rootPanel->m_childs.moveToFront(it); } -equi::Panel* CUIManager::GetTopPanel() const +Panel* CUIManager::GetTopPanel() const { auto it = m_rootPanel->m_childs.begin(); if (it.atEnd()) @@ -191,7 +203,7 @@ equi::Panel* CUIManager::GetTopPanel() const return static_cast(*it); } -void CUIManager::DumpPanelsToConsole() +void CUIManager::DumpPanelsToConsole() const { MsgInfo("Available UI panels/windows:\n"); @@ -234,9 +246,9 @@ IUIControl* CUIManager::GetMouseOver() const bool CUIManager::IsWindowsVisible() const { - for(int i = 0; i < m_panels.numElem(); i++) + for(const Panel* panel : m_panels) { - if(m_panels[i]->IsVisible() && m_panels[i]->m_windowControls) + if(panel->IsVisible() && panel->m_windowControls) return true; } @@ -269,14 +281,14 @@ void CUIManager::Render(ITexture* colorTarget) m_rootPanel->SetRectangle( m_viewFrameRect ); m_rootPanel->ResetSizeDiffs(); - equi::ControlRenderContext ctrlRenderContext{ rendPassRecorder }; + ControlRenderContext ctrlRenderContext{ rendPassRecorder }; m_rootPanel->Render(1, ctrlRenderContext); rendPassRecorder->DbgPopGroup(); g_matSystem->QueueCommandBuffer(rendPassRecorder->End()); } -equi::Panel* CUIManager::GetPanelByElement(IUIControl* control) +Panel* CUIManager::GetPanelByElement(IUIControl* control) const { IUIControl* firstCtrl = control; while(firstCtrl && arrayFindIndex(m_panels, (Panel*)firstCtrl) == -1) diff --git a/shared/equi/EqUI_Manager.h b/shared/equi/EqUI_Manager.h index cf9167d2c..d82f35e29 100644 --- a/shared/equi/EqUI_Manager.h +++ b/shared/equi/EqUI_Manager.h @@ -18,39 +18,37 @@ namespace equi { class Panel; -using ControlFactoryFunc = IUIControl* (*)(); - -struct ControlFactory -{ - const char* name; - ControlFactoryFunc factory; -}; +using ControlFactoryFn = IUIControl* (*)(); +using LayoutFileLoaderFn = EqFunction; class CUIManager { public: - CUIManager(); ~CUIManager(); + CUIManager() = default; void Init(); void Shutdown(); - equi::Panel* GetRootPanel() const; + Panel* GetRootPanel() const; // the element loader - void RegisterFactory(const char* name, ControlFactoryFunc factory); + void RegisterFactory(const char* name, ControlFactoryFn factory); template T* Create(); - IUIControl* CreateElement( const char* pszTypeName ); + IUIControl* CreateElement( const char* pszTypeName ) const; - void AddPanel( equi::Panel* panel); - void DestroyPanel( equi::Panel* pPanel ); - equi::Panel* FindPanel( const char* pszPanelName ) const; + // names of registered control classes in registration order + void GetControlClassNames(Array& outNames) const; - void BringToTop( equi::Panel* panel ); - equi::Panel* GetTopPanel() const; + void AddPanel( Panel* panel); + void DestroyPanel( Panel* pPanel ); + Panel* FindPanel( const char* pszPanelName ) const; + + void BringToTop( Panel* panel ); + Panel* GetTopPanel() const; void SetViewFrame(const IAARectangle& rect); const IAARectangle& GetViewFrame() const; @@ -69,29 +67,37 @@ class CUIManager bool ProcessMouseEvents(float x, float y, int nMouseButtons, int flags); bool ProcessKeyboardEvents(int nKeyButtons, int flags); - void DumpPanelsToConsole(); + void DumpPanelsToConsole() const; + + // loads layout file included with 'child file', the loader replaces the file system when set (used by the editor) + bool LoadLayoutFile(const char* fileName, KVSection& outSec) const; + void SetLayoutFileLoader(const LayoutFileLoaderFn& loader) { m_layoutFileLoader = loader; } IEqFont* GetDefaultFont() const {return m_defaultFont;} private: - equi::Panel* GetPanelByElement(IUIControl* control); - - equi::Panel* m_rootPanel; + struct Factory + { + const char* name; + ControlFactoryFn factory; + }; - IUIControl* m_keyboardFocus; - IUIControl* m_mouseOver; + Panel* GetPanelByElement(IUIControl* control) const; - IVector2D m_mousePos; + Array m_controlFactory{ PP_SL }; + IEqFont* m_defaultFont{ nullptr }; - Array m_panels{ PP_SL }; + Panel* m_rootPanel{ nullptr }; + Array m_panels{ PP_SL }; - IAARectangle m_viewFrameRect; - IMaterial* m_material; + IUIControl* m_keyboardFocus{ nullptr }; + IUIControl* m_mouseOver{ nullptr }; - IEqFont* m_defaultFont; + LayoutFileLoaderFn m_layoutFileLoader; - Array m_controlFactory{ PP_SL }; + IAARectangle m_viewFrameRect{ 0, 0, 640, 480 }; + IVector2D m_mousePos{ 0 }; }; template diff --git a/shared/equi/IEqUI_Control.cpp b/shared/equi/IEqUI_Control.cpp index 29d22ce36..f474aa232 100644 --- a/shared/equi/IEqUI_Control.cpp +++ b/shared/equi/IEqUI_Control.cpp @@ -350,7 +350,7 @@ void IUIControl::InitChildItems(const KVSection& sec, bool keepElements) if (!childClass.CompareCaseIns("file")) { KVSection section; - if (!KV_LoadFromFile(childName, SP_MOD, section)) + if (!equi::Manager->LoadLayoutFile(childName, section)) { MsgWarning("EqUI warning: file %s requested by element %s not found\n", childName.ToCString(), m_name.ToCString()); continue; @@ -787,7 +787,7 @@ void IUIControl::Render(int depth, RenderContextAbstract& context) // should we instead contain elements inside of parent scissor rectangle? rendPassRecorder->SetScissorRectangle(scissorRect); - if( m_parent && m_selfVisible ) + if( m_selfVisible ) { // paint control itself DrawSelf(clientRectRender, rendPassRecorder); @@ -949,21 +949,23 @@ int IUIControl::CommandCb(IUIControl* control, const EvtHandler& event, void* us else if (!UICMD_ARGV(0).CompareCaseIns("engine")) { // execute console commands - g_consoleCommands->SetCommandBuffer(UICMD_ARGV(1).ToCString()); + g_consoleCommands->SetCommandBuffer(UICMD_ARGV(1)); g_consoleCommands->ExecuteCommandBuffer(); g_consoleCommands->ClearCommandBuffer(); } else if (!UICMD_ARGV(0).CompareCaseIns("showpanel")) { // show panel - equi::Panel* panel = equi::Manager->FindPanel(UICMD_ARGV(1).ToCString()); - panel->Show(); + equi::Panel* panel = equi::Manager->FindPanel(UICMD_ARGV(1)); + if(panel) + panel->Show(); } else if (!UICMD_ARGV(0).CompareCaseIns("hidepanel")) { // hide panel - equi::Panel* panel = equi::Manager->FindPanel(UICMD_ARGV(1).ToCString()); - panel->Hide(); + equi::Panel* panel = equi::Manager->FindPanel(UICMD_ARGV(1)); + if (panel) + panel->Hide(); } // TODO: findChild/hideChild etc diff --git a/shared/equi/IEqUI_Control.h b/shared/equi/IEqUI_Control.h index 9551045cd..bef169a4e 100644 --- a/shared/equi/IEqUI_Control.h +++ b/shared/equi/IEqUI_Control.h @@ -254,7 +254,7 @@ class IUIControl : public WeakRefObject IUIControl* m_parent{ nullptr }; List m_childs{ PP_SL }; // child panels - Array m_eventCallbacks{ PP_SL }; + Array m_eventCallbacks{ PP_SL }; // DEPRECATED, not used - might be removed or reworked FontCollection m_fontCollection{ PP_SL }; FontProps m_font; diff --git a/shared/imgui_backend/imgui_host.cpp b/shared/imgui_backend/imgui_host.cpp index 8d78d2abc..533f940a3 100644 --- a/shared/imgui_backend/imgui_host.cpp +++ b/shared/imgui_backend/imgui_host.cpp @@ -5,9 +5,12 @@ #include "core/ConCommand.h" #ifdef IMGUI_ENABLED + #include #include +#include "utils/KeyValues.h" + #include "imgui_impl_matsystem.h" #include "imgui_impl_sys.h" #include "imgui_internal.h" @@ -19,6 +22,204 @@ static CEqImGuiHost s_imGuiHost; CEqImGuiHost* g_imGuiHost = &s_imGuiHost; #ifdef IMGUI_ENABLED + +static ImVec4 ParseImGuiColorValue(const KVSection& key) +{ + const char* valueStr = key.ValueAt(0).GetString(); + if (key.ValueCount() == 1 && valueStr[0] == '#') + { + MColor hexColor = hexToColor4(valueStr + 1); + return ImVec4(hexColor.r, hexColor.g, hexColor.b, hexColor.a); + } + + if (key.ValueCount() < 3) + MsgWarning("ImGui style: '%s' needs RGB or RGBA values\n", key.GetName()); + + ImVec4 color(0.0f, 0.0f, 0.0f, 1.0f); + key.GetValues(color.x, color.y, color.z, color.w); + return color; +} + +// Descriptor value that writes the parsed entry directly into an ImGuiStyle member +template +struct ImGuiStyleValue +{ + T* ptr{ nullptr }; + void operator=(const T& value) const { *ptr = value; } +}; + +template +struct KVPairValuesGetter> +{ + using CompGetter = KVPairValuesGetter; + static float Get(const KVSection& section, int index) { return CompGetter::Get(section, index); } + static const int vcount = 1; +}; + +template<> struct KVPairValuesGetter> +{ + using CompGetter = KVPairValuesGetter; + static ImVec2 Get(const KVSection& section, int index) + { + return ImVec2(CompGetter::Get(section, index), CompGetter::Get(section, index + 1)); + } + static const int vcount = 2; +}; + +template<> struct KVPairValuesGetter> +{ + static ImVec4 Get(const KVSection& section, int index) { return ParseImGuiColorValue(section); } + static const int vcount = 1; +}; + +#define IMGUI_STYLE_COLOR_LIST(X) \ + X(Text) \ + X(TextDisabled) \ + X(WindowBg) \ + X(ChildBg) \ + X(PopupBg) \ + X(Border) \ + X(BorderShadow) \ + X(FrameBg) \ + X(FrameBgHovered) \ + X(FrameBgActive) \ + X(TitleBg) \ + X(TitleBgActive) \ + X(TitleBgCollapsed) \ + X(MenuBarBg) \ + X(ScrollbarBg) \ + X(ScrollbarGrab) \ + X(ScrollbarGrabHovered) \ + X(ScrollbarGrabActive) \ + X(CheckMark) \ + X(SliderGrab) \ + X(SliderGrabActive) \ + X(Button) \ + X(ButtonHovered) \ + X(ButtonActive) \ + X(Header) \ + X(HeaderHovered) \ + X(HeaderActive) \ + X(Separator) \ + X(SeparatorHovered) \ + X(SeparatorActive) \ + X(ResizeGrip) \ + X(ResizeGripHovered) \ + X(ResizeGripActive) \ + X(Tab) \ + X(TabHovered) \ + X(TabActive) \ + X(TabUnfocused) \ + X(TabUnfocusedActive) \ + X(PlotLines) \ + X(PlotLinesHovered) \ + X(PlotHistogram) \ + X(PlotHistogramHovered) \ + X(TableHeaderBg) \ + X(TableBorderStrong) \ + X(TableBorderLight) \ + X(TableRowBg) \ + X(TableRowBgAlt) \ + X(TextSelectedBg) \ + X(DragDropTarget) \ + X(NavHighlight) \ + X(NavWindowingHighlight) \ + X(NavWindowingDimBg) \ + X(ModalWindowDimBg) + +#define IMGUI_STYLE_FIELD_LIST(X) \ + X(Alpha, float) \ + X(DisabledAlpha, float) \ + X(WindowPadding, ImVec2) \ + X(WindowRounding, float) \ + X(WindowBorderSize, float) \ + X(WindowMinSize, ImVec2) \ + X(WindowTitleAlign, ImVec2) \ + X(WindowMenuButtonPosition, int) \ + X(ChildRounding, float) \ + X(ChildBorderSize, float) \ + X(PopupRounding, float) \ + X(PopupBorderSize, float) \ + X(FramePadding, ImVec2) \ + X(FrameRounding, float) \ + X(FrameBorderSize, float) \ + X(ItemSpacing, ImVec2) \ + X(ItemInnerSpacing, ImVec2) \ + X(CellPadding, ImVec2) \ + X(TouchExtraPadding, ImVec2) \ + X(IndentSpacing, float) \ + X(ColumnsMinSpacing, float) \ + X(ScrollbarSize, float) \ + X(ScrollbarRounding, float) \ + X(GrabMinSize, float) \ + X(GrabRounding, float) \ + X(LogSliderDeadzone, float) \ + X(TabRounding, float) \ + X(TabBorderSize, float) \ + X(TabMinWidthForCloseButton, float) \ + X(ColorButtonPosition, int) \ + X(ButtonTextAlign, ImVec2) \ + X(SelectableTextAlign, ImVec2) \ + X(DisplayWindowPadding, ImVec2) \ + X(DisplaySafeAreaPadding, ImVec2) \ + X(MouseCursorScale, float) \ + X(AntiAliasedLines, bool) \ + X(AntiAliasedLinesUseTex, bool) \ + X(AntiAliasedFill, bool) \ + X(CurveTessellationTol, float) \ + X(CircleTessellationMaxError, float) + +#define IMGUI_STYLE_COLOR_DECLARE(name) ImGuiStyleValue name{ &ImGui::GetStyle().Colors[ImGuiCol_##name] }; +#define IMGUI_STYLE_COLOR_DESC(name) KV_DESC_FIELD(name) +#define IMGUI_STYLE_FIELD_DECLARE(name, type) ImGuiStyleValue name{ &ImGui::GetStyle().name }; +#define IMGUI_STYLE_FIELD_DESC(name, type) KV_DESC_FIELD(name) + +#define IMGUI_STYLE_COLOR_COUNT(name) + 1 +static_assert(IMGUI_STYLE_COLOR_LIST(IMGUI_STYLE_COLOR_COUNT) == ImGuiCol_COUNT, "ImGui color list is out of date"); +#undef IMGUI_STYLE_COLOR_COUNT + +struct ImGuiStyleColorsDesc +{ + DEFINE_KEYVALUES_DESC(); + + IMGUI_STYLE_COLOR_LIST(IMGUI_STYLE_COLOR_DECLARE) +}; + +BEGIN_KEYVALUES_DESC(ImGuiStyleColorsDesc) + IMGUI_STYLE_COLOR_LIST(IMGUI_STYLE_COLOR_DESC) +END_KEYVALUES_DESC + +struct ImGuiStyleDesc +{ + DEFINE_KEYVALUES_DESC(); + + ImGuiStyleColorsDesc Colors; + + IMGUI_STYLE_FIELD_LIST(IMGUI_STYLE_FIELD_DECLARE) +}; + +BEGIN_KEYVALUES_DESC(ImGuiStyleDesc) + KV_DESC_EMBEDDED(Colors) + IMGUI_STYLE_FIELD_LIST(IMGUI_STYLE_FIELD_DESC) +END_KEYVALUES_DESC + +#undef IMGUI_STYLE_COLOR_DECLARE +#undef IMGUI_STYLE_COLOR_DESC +#undef IMGUI_STYLE_FIELD_DECLARE +#undef IMGUI_STYLE_FIELD_DESC +#undef IMGUI_STYLE_COLOR_LIST +#undef IMGUI_STYLE_FIELD_LIST + +static void LoadImGuiStyle(const char* fileName) +{ + KVSection styleKvs; + if (!KV_LoadFromFile(fileName, SP_DATA | SP_MOD, styleKvs)) + return; + + ImGuiStyleDesc styleDesc; + KeyValues::ParseDesc(styleDesc, styleKvs); +} + static void ImGuiBeginMenuPath(const char* path, bool& selected) { char tmpName[128] = { 0 }; @@ -69,15 +270,11 @@ void CEqImGuiHost::Init(EQWNDHANDLE window) //io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls //io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls - // Setup Dear ImGui style - ImGui::StyleColorsDark(); - // Setup Platform/Renderer backends ImGui_ImplMatSystem_Init(); static const char* fontFileName = "resources/imgui/Inter-Regular.ttf"; IFileStreamPtr fontFile = g_fileSystem->Open(fontFileName, FS_OPEN_READ, SP_DATA); - if (fontFile) { const int length = fontFile->GetSize(); @@ -91,6 +288,8 @@ void CEqImGuiHost::Init(EQWNDHANDLE window) io.Fonts->AddFontFromMemoryTTF(buffer, length, 16.0f, &font_cfg, io.Fonts->GetGlyphRangesCyrillic()); } + LoadImGuiStyle("resources/imgui/style.txt"); + // TODO: ImGui networked console port (for Android) if(window) { diff --git a/shared/imgui_backend/imgui_impl_matsystem.cpp b/shared/imgui_backend/imgui_impl_matsystem.cpp index 67efeb80f..94ea2e22c 100644 --- a/shared/imgui_backend/imgui_impl_matsystem.cpp +++ b/shared/imgui_backend/imgui_impl_matsystem.cpp @@ -10,9 +10,8 @@ // DirectX data struct ImGui_ImplMatSystem_Data { - ITexturePtr FontTexture; - - ImGui_ImplMatSystem_Data() { memset(this, 0, sizeof(*this)); } + ITexturePtr FontTexture; + EShaderBlendMode BlendMode{ SHADER_BLEND_TRANSLUCENT }; // set by BeginOpaque / EndOpaque callbacks }; // Backend data stored in io.BackendRendererUserData to allow support for multiple Dear ImGui contexts @@ -22,6 +21,30 @@ static ImGui_ImplMatSystem_Data* ImGui_ImplMatSystem_GetBackendData() return ImGui::GetCurrentContext() ? (ImGui_ImplMatSystem_Data*)ImGui::GetIO().BackendRendererUserData : nullptr; } +// callback data is non-null for BeginOpaque +static void ImGui_ImplMatSystem_OpaqueCallback(const ImDrawList* parent_list, const ImDrawCmd* cmd) +{ + ImGui_ImplMatSystem_Data* bd = ImGui_ImplMatSystem_GetBackendData(); + bd->BlendMode = cmd->UserCallbackData ? SHADER_BLEND_NONE : SHADER_BLEND_TRANSLUCENT; +} + +void ImGui_ImplMatSystem_BeginOpaque(ImDrawList* drawList) +{ + drawList->AddCallback(ImGui_ImplMatSystem_OpaqueCallback, reinterpret_cast(1)); +} + +void ImGui_ImplMatSystem_EndOpaque(ImDrawList* drawList) +{ + drawList->AddCallback(ImGui_ImplMatSystem_OpaqueCallback, nullptr); +} + +void ImGui_ImplMatSystem_AddImageOpaque(ImDrawList* drawList, ImTextureID texture, const ImVec2& pMin, const ImVec2& pMax, const ImVec2& uvMin, const ImVec2& uvMax, ImU32 col) +{ + ImGui_ImplMatSystem_BeginOpaque(drawList); + drawList->AddImage(texture, pMin, pMax, uvMin, uvMax, col); + ImGui_ImplMatSystem_EndOpaque(drawList); +} + static void ImGui_ImplMatSystem_SetupRenderState(ImDrawData* draw_data, IGPURenderPassRecorder* rendPassRecorder, const RenderDrawCmd& drawCmd) { MatSysDefaultRenderPass defaultRenderPass; @@ -34,24 +57,34 @@ static void ImGui_ImplMatSystem_SetupRenderState(ImDrawData* draw_data, IGPURend rendPassRecorder->SetIndexBufferView(drawCmd.instanceInfo.indexBuffer, drawCmd.instanceInfo.indexFormat); } -// Render function. -void ImGui_ImplMatSystem_RenderDrawData(ImDrawData* draw_data, IGPURenderPassRecorder* rendPassRecorder) +// Dynamic mesh capacity is lower than a draw list can have (MAX_DYNAMIC_VERTICES and MAX_DYNAMIC_INDICES +// in DynamicMesh.cpp), draw commands are split by triangles into chunks and chunks are packed into meshes +static constexpr int s_maxBatchVertices = 32000; +static constexpr int s_maxBatchIndices = 31998; // whole triangles + +// part of a draw command that fits into one dynamic mesh +struct ImGui_ImplMatSystem_DrawChunk { - // Avoid rendering when minimized - if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) - return; + const ImDrawCmd* cmd; + int firstIndex; // in the draw list index buffer + int numIndices; + int minVertex; // referenced range of the draw list vertex buffer + int maxVertex; +}; - // Copy vertices to matsystem mesh - ImVec2 clip_off = draw_data->DisplayPos; - for (int n = 0; n < draw_data->CmdListsCount; n++) - { - const ImDrawList* cmd_list = draw_data->CmdLists[n]; +static void ImGui_ImplMatSystem_DrawChunks(ImDrawData* draw_data, IGPURenderPassRecorder* rendPassRecorder, const ImDrawList* cmd_list, ArrayCRef chunks, int numVertices, int numIndices, RenderDrawCmd& drawCmd) +{ + if (chunks.isEmpty()) + return; - IDynamicMeshPtr dynMesh = g_matSystem->GetDynamicMesh(max(cmd_list->VtxBuffer.Size, cmd_list->IdxBuffer.Size)); - CMeshBuilder mb(dynMesh); - mb.Begin(PRIM_TRIANGLES); + IDynamicMeshPtr dynMesh = g_matSystem->GetDynamicMesh(max(numVertices, numIndices)); + CMeshBuilder mb(dynMesh); + mb.Begin(PRIM_TRIANGLES); - for (int i = 0; i < cmd_list->VtxBuffer.Size; i++) + // vertices of each chunk go one after another + for (const ImGui_ImplMatSystem_DrawChunk& chunk : chunks) + { + for (int i = chunk.minVertex; i <= chunk.maxVertex; i++) { const ImDrawVert& vtx_src = cmd_list->VtxBuffer.Data[i]; MColor color(vtx_src.col); @@ -61,54 +94,159 @@ void ImGui_ImplMatSystem_RenderDrawData(ImDrawData* draw_data, IGPURenderPassRec mb.Color4fv(color); mb.AdvanceVertex(); } + } + // indices are rebased to the chunk vertices + int baseVertex = 0; + for (const ImGui_ImplMatSystem_DrawChunk& chunk : chunks) + { ImDrawIdx* idx_dst; void* vtx_dst; - if (dynMesh->AllocateGeom(0, cmd_list->IdxBuffer.Size, &vtx_dst, &idx_dst, false) != -1) + if (dynMesh->AllocateGeom(0, chunk.numIndices, &vtx_dst, &idx_dst, false) != -1) { - memcpy(idx_dst, cmd_list->IdxBuffer.Data, cmd_list->IdxBuffer.Size * sizeof(ImDrawIdx)); + const ImDrawIdx* idx_src = cmd_list->IdxBuffer.Data + chunk.firstIndex; + const int indexOffset = chunk.cmd->VtxOffset - chunk.minVertex + baseVertex; + for (int i = 0; i < chunk.numIndices; i++) + idx_dst[i] = static_cast(idx_src[i] + indexOffset); } + baseVertex += chunk.maxVertex - chunk.minVertex + 1; + } + + if (!mb.End(drawCmd)) + return; - RenderDrawCmd drawCmd; - mb.End(drawCmd); + const ImVec2 clip_off = draw_data->DisplayPos; + int firstIndex = 0; + for (const ImGui_ImplMatSystem_DrawChunk& chunk : chunks) + { + const ImDrawCmd* pcmd = chunk.cmd; + const int chunkFirstIndex = firstIndex; + firstIndex += chunk.numIndices; + + // Project scissor/clipping rectangles into framebuffer space + ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); + ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); + if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) + continue; + + // Apply Scissor/clipping rectangle, Bind texture, Draw + + MatSysDefaultRenderPass defaultRenderPass; + defaultRenderPass.blendMode = ImGui_ImplMatSystem_GetBackendData()->BlendMode; + defaultRenderPass.texture = ITexturePtr((ITexture*)pcmd->GetTexID()); + + g_matSystem->SetupMaterialPipeline(g_matSystem->GetDefaultMaterial(), nullptr, drawCmd.batchInfo.primTopology, drawCmd.instanceInfo.instFormat, RenderPassContext(rendPassRecorder, &defaultRenderPass)); + + rendPassRecorder->SetScissorRectangle(IAARectangle((int)clip_min.x, (int)clip_min.y, (int)clip_max.x, (int)clip_max.y)); + rendPassRecorder->SetVertexBufferView(0, drawCmd.instanceInfo.vertexBuffers[0]); + rendPassRecorder->SetIndexBufferView(drawCmd.instanceInfo.indexBuffer, INDEXFMT_UINT16); + rendPassRecorder->DrawIndexed(chunk.numIndices, chunkFirstIndex, 1); + } +} + +// Render function. +void ImGui_ImplMatSystem_RenderDrawData(ImDrawData* draw_data, IGPURenderPassRecorder* rendPassRecorder) +{ + // Avoid rendering when minimized + if (draw_data->DisplaySize.x <= 0.0f || draw_data->DisplaySize.y <= 0.0f) + return; + + ImGui_ImplMatSystem_Data* bd = ImGui_ImplMatSystem_GetBackendData(); + + Array chunks(PP_SL); + RenderDrawCmd drawCmd; + + for (int n = 0; n < draw_data->CmdListsCount; n++) + { + const ImDrawList* cmd_list = draw_data->CmdLists[n]; + bd->BlendMode = SHADER_BLEND_TRANSLUCENT; + + // chunks waiting for a mesh + int batchVertices = 0; + int batchIndices = 0; + chunks.clear(); + + auto flushChunks = [&]() { + ImGui_ImplMatSystem_DrawChunks(draw_data, rendPassRecorder, cmd_list, chunks, batchVertices, batchIndices, drawCmd); + chunks.clear(); + batchVertices = 0; + batchIndices = 0; + }; + + auto addChunk = [&](const ImGui_ImplMatSystem_DrawChunk& chunk) { + const int chunkVertices = chunk.maxVertex - chunk.minVertex + 1; + if (batchVertices + chunkVertices > s_maxBatchVertices || batchIndices + chunk.numIndices > s_maxBatchIndices) + flushChunks(); + + chunks.append(chunk); + batchVertices += chunkVertices; + batchIndices += chunk.numIndices; + }; - // render command buffers now for (int cmd_i = 0; cmd_i < cmd_list->CmdBuffer.Size; cmd_i++) { const ImDrawCmd* pcmd = &cmd_list->CmdBuffer[cmd_i]; if (pcmd->UserCallback != nullptr) { + // commands before the callback are drawn first + flushChunks(); + // User callback, registered via ImDrawList::AddCallback() // (ImDrawCallback_ResetRenderState is a special callback value used by the user to request the renderer to reset render state.) if (pcmd->UserCallback == ImDrawCallback_ResetRenderState) - ImGui_ImplMatSystem_SetupRenderState(draw_data, rendPassRecorder, drawCmd); + { + bd->BlendMode = SHADER_BLEND_TRANSLUCENT; + if (drawCmd.instanceInfo.vertexBuffers[0].buffer) + ImGui_ImplMatSystem_SetupRenderState(draw_data, rendPassRecorder, drawCmd); + } else pcmd->UserCallback(cmd_list, pcmd); - } - else - { - // Project scissor/clipping rectangles into framebuffer space - ImVec2 clip_min(pcmd->ClipRect.x - clip_off.x, pcmd->ClipRect.y - clip_off.y); - ImVec2 clip_max(pcmd->ClipRect.z - clip_off.x, pcmd->ClipRect.w - clip_off.y); - if (clip_max.x <= clip_min.x || clip_max.y <= clip_min.y) - continue; - - // Apply Scissor/clipping rectangle, Bind texture, Draw - MatSysDefaultRenderPass defaultRenderPass; - defaultRenderPass.blendMode = SHADER_BLEND_TRANSLUCENT; - defaultRenderPass.texture = ITexturePtr((ITexture*)pcmd->GetTexID()); + continue; + } - g_matSystem->SetupMaterialPipeline(g_matSystem->GetDefaultMaterial(), nullptr, drawCmd.batchInfo.primTopology, drawCmd.instanceInfo.instFormat, RenderPassContext(rendPassRecorder, &defaultRenderPass)); + // split the command by triangles into chunks that fit into one mesh + ImGui_ImplMatSystem_DrawChunk chunk; + chunk.cmd = pcmd; + chunk.firstIndex = pcmd->IdxOffset; + chunk.numIndices = 0; + chunk.minVertex = INT_MAX; + chunk.maxVertex = -1; - rendPassRecorder->SetScissorRectangle(IAARectangle((int)clip_min.x, (int)clip_min.y, (int)clip_max.x, (int)clip_max.y)); - rendPassRecorder->SetVertexBufferView(0, drawCmd.instanceInfo.vertexBuffers[0]); - rendPassRecorder->SetIndexBufferView(drawCmd.instanceInfo.indexBuffer, INDEXFMT_UINT16); - rendPassRecorder->DrawIndexed(pcmd->ElemCount, pcmd->IdxOffset, 1); + const ImDrawIdx* indices = cmd_list->IdxBuffer.Data; + for (int i = pcmd->IdxOffset; i + 2 < (int)(pcmd->IdxOffset + pcmd->ElemCount); i += 3) + { + const int triMin = min(min(indices[i], indices[i + 1]), indices[i + 2]) + pcmd->VtxOffset; + const int triMax = max(max(indices[i], indices[i + 1]), indices[i + 2]) + pcmd->VtxOffset; + + const int minVertex = min(chunk.minVertex, triMin); + const int maxVertex = max(chunk.maxVertex, triMax); + if (chunk.numIndices && (maxVertex - minVertex + 1 > s_maxBatchVertices || chunk.numIndices + 3 > s_maxBatchIndices)) + { + addChunk(chunk); + + chunk.firstIndex = i; + chunk.numIndices = 0; + chunk.minVertex = triMin; + chunk.maxVertex = triMax; + } + else + { + chunk.minVertex = minVertex; + chunk.maxVertex = maxVertex; + } + chunk.numIndices += 3; } + + if (chunk.numIndices) + addChunk(chunk); } + + flushChunks(); } + + bd->BlendMode = SHADER_BLEND_TRANSLUCENT; } bool ImGui_ImplMatSystem_Init() @@ -120,7 +258,7 @@ bool ImGui_ImplMatSystem_Init() ImGui_ImplMatSystem_Data* bd = IM_NEW(ImGui_ImplMatSystem_Data)(); io.BackendRendererUserData = (void*)bd; io.BackendRendererName = "eqMatSystem"; - //io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. + io.BackendFlags |= ImGuiBackendFlags_RendererHasVtxOffset; // We can honor the ImDrawCmd::VtxOffset field, allowing for large meshes. return true; } diff --git a/shared/imgui_backend/imgui_impl_matsystem.h b/shared/imgui_backend/imgui_impl_matsystem.h index 5d4a57bd8..9777f83f4 100644 --- a/shared/imgui_backend/imgui_impl_matsystem.h +++ b/shared/imgui_backend/imgui_impl_matsystem.h @@ -12,3 +12,11 @@ IMGUI_IMPL_API void ImGui_ImplMatSystem_RenderDrawData(ImDrawData* draw_data // Use if you want to reset your rendering device without losing Dear ImGui state. IMGUI_IMPL_API bool ImGui_ImplMatSystem_CreateDeviceObjects(); IMGUI_IMPL_API void ImGui_ImplMatSystem_InvalidateDeviceObjects(); + +// Draw list commands between BeginOpaque and EndOpaque are drawn without blending, so texture alpha +// is ignored (render targets whose alpha is not coverage). Blending is restored for each draw list. +IMGUI_IMPL_API void ImGui_ImplMatSystem_BeginOpaque(ImDrawList* drawList); +IMGUI_IMPL_API void ImGui_ImplMatSystem_EndOpaque(ImDrawList* drawList); + +// AddImage drawn without blending +IMGUI_IMPL_API void ImGui_ImplMatSystem_AddImageOpaque(ImDrawList* drawList, ImTextureID texture, const ImVec2& pMin, const ImVec2& pMax, const ImVec2& uvMin = ImVec2(0, 0), const ImVec2& uvMax = ImVec2(1, 1), ImU32 col = IM_COL32_WHITE); diff --git a/shared/sys/scripting/sys_esl.cpp b/shared/sys/scripting/sys_esl.cpp index 2b8ce5fdf..2b03ff7b2 100644 --- a/shared/sys/scripting/sys_esl.cpp +++ b/shared/sys/scripting/sys_esl.cpp @@ -477,4 +477,5 @@ void eslSysTerm() { eslSysConsoleTerm(); eslSysInputTerm(); + eslSysEquiTerm(); } \ No newline at end of file diff --git a/shared/sys/scripting/sys_esl_equi.cpp b/shared/sys/scripting/sys_esl_equi.cpp index 89904da4c..8f69cce83 100644 --- a/shared/sys/scripting/sys_esl_equi.cpp +++ b/shared/sys/scripting/sys_esl_equi.cpp @@ -12,6 +12,12 @@ #include "sys_esl.h" #include "sys_esl_equi.h" +#include "equi/EqUI_Editor.h" + +#ifdef IMGUI_ENABLED +#include "imgui_backend/imgui_host.h" +#endif + EQSCRIPT_TYPE_BEGIN(EqWString) EQSCRIPT_TYPE_END @@ -193,7 +199,7 @@ EQSCRIPT_TYPE_BEGIN(equi::IUIControl) EQSCRIPT_BIND_VAR_EX_GET_SET(label, GetLabel, SetLabel) EQSCRIPT_BIND_VAR_EX_GET_SET(visible, IsVisible, SetVisible) EQSCRIPT_BIND_VAR_EX_GET_SET(selfVisible, IsSelfVisible, SetSelfVisible) - EQSCRIPT_BIND_VAR_EX_GET_SET(clipChilds, SetClipsChilds, SetClipsChilds) + EQSCRIPT_BIND_VAR_EX_GET_SET(clipChilds, IsClipsChilds, SetClipsChilds) EQSCRIPT_BIND_VAR_EX_GET_SET(clipTransform, HasClipTransform, SetClipTransform) EQSCRIPT_BIND_VAR_EX_GET_SET(enabled, IsEnabled, Enable) EQSCRIPT_BIND_VAR_EX_GET_SET(size, GetSize, SetSize) @@ -437,5 +443,14 @@ bool eslSysEquiInit(const esl::ScriptState& state) state.SetGlobal("fonts", fontsTable); } +#ifdef IMGUI_ENABLED + g_imGuiHost->AddDebugMenu("ENGINE/EQUI/SCHEME EDITOR UI", equiEditorUIDraw); +#endif + return true; +} + +void eslSysEquiTerm() +{ + equiEditorShutdown(); } \ No newline at end of file diff --git a/shared/sys/scripting/sys_esl_equi.h b/shared/sys/scripting/sys_esl_equi.h index 1c9f9fdc9..05bbcb903 100644 --- a/shared/sys/scripting/sys_esl_equi.h +++ b/shared/sys/scripting/sys_esl_equi.h @@ -33,4 +33,5 @@ EQSCRIPT_BIND_TYPE_WITH_PARENT(equi::Container, equi::IUIControl, "equi::Contain EQSCRIPT_BIND_TYPE_WITH_PARENT(equi::Image, equi::IUIControl, "equi::Image") EQSCRIPT_BIND_TYPE_WITH_PARENT(equi::ProgressBar, equi::IUIControl, "equi::ProgressBar") -bool eslSysEquiInit(const esl::ScriptState& state); \ No newline at end of file +bool eslSysEquiInit(const esl::ScriptState& state); +void eslSysEquiTerm(); \ No newline at end of file diff --git a/shared/sys/sys_host.cpp b/shared/sys/sys_host.cpp index 7c367d710..6ab61338d 100644 --- a/shared/sys/sys_host.cpp +++ b/shared/sys/sys_host.cpp @@ -296,6 +296,22 @@ bool CGameHost::LoadModules() void CGameHost::SetWindowTitle(const char* windowTitle) { m_windowTitle = windowTitle; + ApplyWindowTitle(); +} + +void CGameHost::ApplyWindowTitle() +{ + // non-retail title shows the renderer, so it is applied after the material system is initialized + if (!m_window || !g_renderAPI) + return; + +#ifdef _RETAIL + SDL_SetWindowTitle(m_window, m_windowTitle); +#else + SDL_SetWindowTitle(m_window, EqString::Format( + "%s | %s | " COMPILE_CONFIGURATION " (" COMPILE_PLATFORM ") | build %d (" COMPILE_DATE ")", + m_windowTitle.ToCString(), g_renderAPI->GetRendererName(), BUILD_NUMBER_ENGINE)); +#endif } bool CGameHost::IsWindowed() const @@ -350,14 +366,22 @@ void CGameHost::SetWindowedMode() int screen = 0; sysHostGetWindowProperties(fullscreen, screen, adjustedWide, adjustedTall); + // maximized window (vid_maximized) keeps its size: resizing it leaves the window maximized but not filling the screen + const bool maximized = (SDL_GetWindowFlags(m_window) & SDL_WINDOW_MAXIMIZED) != 0; + if (maximized) + SDL_GetWindowSize(m_window, &adjustedWide, &adjustedTall); + OnWindowResize(adjustedWide, adjustedTall); if (!g_matSystem->IsInitialized() || g_matSystem->SetWindowed(true)) { Msg("Set %dx%d mode (windowed)\n", adjustedWide, adjustedTall); SDL_SetWindowFullscreen(m_window, 0); - SDL_SetWindowSize(m_window, adjustedWide, adjustedTall); - SDL_SetWindowPosition(m_window, adjustedPosX, adjustedPosY); + if (!maximized) + { + SDL_SetWindowSize(m_window, adjustedWide, adjustedTall); + SDL_SetWindowPosition(m_window, adjustedPosX, adjustedPosY); + } } } @@ -622,13 +646,7 @@ bool CGameHost::InitSystems() REGISTER_INTERNAL_SHADERS(); g_fullScreenEdgeAA->Init(); -#ifdef _RETAIL - SDL_SetWindowTitle(m_window, m_windowTitle); -#else - SDL_SetWindowTitle(m_window, EqString::Format( - "%s | %s | " COMPILE_CONFIGURATION " (" COMPILE_PLATFORM ") | build %d (" COMPILE_DATE ")", - m_windowTitle.ToCString(), g_renderAPI->GetRendererName(), BUILD_NUMBER_ENGINE)); -#endif + ApplyWindowTitle(); return true; } diff --git a/shared/sys/sys_host.h b/shared/sys/sys_host.h index fe960e444..f7d8f1015 100644 --- a/shared/sys/sys_host.h +++ b/shared/sys/sys_host.h @@ -99,6 +99,7 @@ class CGameHost void UpdateCursorState(); void SetCursorShow(bool bShow); + void ApplyWindowTitle(); void BeginScene(); void EndScene(); diff --git a/shared/sys/sys_viewport.cpp b/shared/sys/sys_viewport.cpp index 5402b5816..c6ca1b853 100644 --- a/shared/sys/sys_viewport.cpp +++ b/shared/sys/sys_viewport.cpp @@ -14,6 +14,7 @@ // enabled by sys_viewport.h #ifdef HOST_VIEWPORT_ENABLED #include +#include "imgui_backend/imgui_impl_matsystem.h" #endif // HOST_VIEWPORT_ENABLED CHostViewport::CHostViewport(const char* name) @@ -51,11 +52,13 @@ void CHostViewport::UpdateTargets(const IVector2D& size) .End() ); + // states sample scene depth like the default depth buffer m_depthTarget = g_renderAPI->CreateRenderTarget( Builder() .Name(EqString::Format("_hvp_%s_depth", m_name)) .Size(size.x, size.y) .Format(g_matSystem->GetDefaultDepthBuffer()->GetFormat()) + .Sampler(sampler) .End() ); return; @@ -85,7 +88,8 @@ void CHostViewport::DrawImGui(const Vector2D& size) m_hovered = ImGui::IsItemHovered() || ImGui::IsItemActive(); m_focused = ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows); - ImGui::GetWindowDrawList()->AddImage(reinterpret_cast(m_colorTarget.Ptr()), screenPos, screenEnd); + // alpha of the state render target is not coverage + ImGui_ImplMatSystem_AddImageOpaque(ImGui::GetWindowDrawList(), reinterpret_cast(m_colorTarget.Ptr()), screenPos, screenEnd); m_screenRect = IAARectangle(static_cast(screenPos.x), static_cast(screenPos.y), static_cast(screenEnd.x), static_cast(screenEnd.y)); m_drawFrame = ImGui::GetFrameCount(); diff --git a/shared/sys/sys_window.cpp b/shared/sys/sys_window.cpp index 3ca425a03..d40848628 100644 --- a/shared/sys/sys_window.cpp +++ b/shared/sys/sys_window.cpp @@ -21,6 +21,7 @@ DECLARE_CVAR(sys_sleep, "1", "Sleep time for every frame", CV_ARCHIVE); DECLARE_CVAR(vid_screen, "0", "Fullscreen mode screen ID", CV_ARCHIVE); DECLARE_CVAR(vid_mode, "1024x768", "Screen Resoulution. Resolution string format: WIDTHxHEIGHT", CV_ARCHIVE); DECLARE_CVAR(vid_fullscreen, "0", "Enable fullscreen mode on startup", CV_ARCHIVE); +DECLARE_CVAR(vid_maximized, "0", "Maximize window at startup", CV_ARCHIVE); #define DEFAULT_WINDOW_TITLE "Initializing..." @@ -58,6 +59,9 @@ EQWNDHANDLE sysHostCreateWindow() if (fullscreen) sdlFlags |= SDL_WINDOW_FULLSCREEN; + if(vid_maximized.GetBool()) + sdlFlags |= SDL_WINDOW_MAXIMIZED; + #ifdef PLAT_ANDROID adjustedPosX = adjustedPosY = SDL_WINDOWPOS_UNDEFINED; diff --git a/src_dependency/nvrhi/NVRHI b/src_dependency/nvrhi/NVRHI index 66a0332ce..d54097996 160000 --- a/src_dependency/nvrhi/NVRHI +++ b/src_dependency/nvrhi/NVRHI @@ -1 +1 @@ -Subproject commit 66a0332cef59a3075305f6dc9d46170724c2ab93 +Subproject commit d5409799680bbe6eb717203966b0bb9f674be444 diff --git a/src_dependency/premake5.lua b/src_dependency/premake5.lua index ca0873e5e..5534b4bd5 100644 --- a/src_dependency/premake5.lua +++ b/src_dependency/premake5.lua @@ -8,7 +8,6 @@ if IS_ANDROID then else include(DependencyPath.openal) include(DependencyPath.libsdl) - include "wxWidgets" end if ENABLE_TESTS then diff --git a/src_dependency/wxWidgets/premake5.lua b/src_dependency/wxWidgets/premake5.lua deleted file mode 100644 index cd8e8263f..000000000 --- a/src_dependency/wxWidgets/premake5.lua +++ /dev/null @@ -1,28 +0,0 @@ -usage "wxWidgets" - filter "system:Linux" - wx_config { - Root=WX_DIR, - Debug="no", - Static="no", - Unicode="no", - Version="3.0", - Libs="core,aui,gl" - } - - filter "system:Windows" - includedirs { - "./include", - "./include/msvc" - } - - defines { "WXUSINGDLL", "wxMSVC_VERSION_AUTO", "wxMSVC_VERSION_ABI_COMPAT" } - - filter "platforms:x64" - libdirs { - "./lib/vc14x_x64_dll" - } - - filter "platforms:x86" - libdirs { - "./lib/vc14x_dll" - } diff --git a/utils/.premake_modules/wxwidgets.lua b/utils/.premake_modules/wxwidgets.lua deleted file mode 100644 index 3a8372650..000000000 --- a/utils/.premake_modules/wxwidgets.lua +++ /dev/null @@ -1,231 +0,0 @@ --- NOTE: --- This script was taken from: https://wiki.wxwidgets.org/Premake4 --- And modified by Miss to fix some issues and work with Premake5 --- --- Use like: --- flags "WinMain" --- configuration "Debug" --- wx_config { Root=WX_DIR, Debug="yes", Static="yes", Unicode="yes", Version="3.1", Libs="core,adv,gl,aui,propgrid" } --- configuration "Release" --- wx_config { Root=WX_DIR, Debug="np", Static="yes", Unicode="yes", Version="3.1", Libs="core,adv,gl,aui,propgrid" } - --- use_wxwidgets.lua --- wxWidgets configuration file for premake4 --- --- authors: laurent.humbertclaude@gmail.com --- v.krishnakumar@gmail.com - --- optional root folder of wxwidgets installation folder --- - -newoption { - trigger = "wx_root", - value = "PATH", - description = "Path to wxwidgets root folder, by default, WXWIN envvar will be used or wx-config found in path on POSIX" -} - -function shellout(str) - if USE_RUNTIME then - return { "`" .. str .. "`" } - end - - local out_links = {} - local out_includes = {} - - ret = {} - local cmd_result = os.outputof(str) or "" - for i in (cmd_result:match'^%s*(.*%S)' or ''):gmatch('%S+') do - if string.sub(i, 1, 2) == "-l" then - table.insert(out_links, string.sub(i, 3)) - elseif string.sub(i, 1, 2) == "-I" then - table.insert(out_includes, string.sub(i, 3)) - else - table.insert(ret, i) - end - end - - links(out_links) - includedirs(out_includes) - - return ret -end - --- The wx_config the parameters are. --- Root : path to wx root folder. Can be left empty if WXWIN is defined --- or if wx-config is accessible. --- Debug : "yes" use debug version of wxwidgets. Default to "no" --- Version : one of '2.4', '2.5', '2.6', '2.7', '2.8', '2.9'. Default to '2.9' --- Static : indicates how wx is to be linked. Values are --- either "yes" for static linking or "no" for shared linking, Default to "no" --- Unicode : use "yes" for unicode or "no" for ansi version. --- ansi version only available up to 2.8 --- Default to "yes" --- Universal : use universal configuration. Default to "no" --- Libs : a list of wx libraries that you want to link with. --- eg: "aui,media,html" --- Default to "richtext,aui,xrc,qa,html,adv,core,xml,net"; base is implicit --- WindowsCompiler : compiler used to compile windows libraries ( "vc" or "gcc" ) - -function wx_config(options) - - local wrongParam = false - local allowedWxOptions = {"Root", "Debug", "Host", "Version", "Static", "Unicode", "Universal", "Libs", "WindowsCompiler" } - for option in pairs(options) do - if not table.contains(allowedWxOptions, option) then - print ("unrecognized option '"..option.. "'") - wrongParam = true - end - end - if wrongParam then print("valid options are : '" .. table.concat(allowedWxOptions, "', '").."'") end - - wx_config_Private( options.Root or "", - options.Debug or "", - options.Host or "", - options.Version or "2.9", - options.Static or "", - options.Unicode or "yes", - options.Universal or "", - options.Libs or "richtext,aui,xrc,qa,html,adv,core,xml,net", -- base is implicit, std not valid - options.WindowsCompiler or "vc" - ) -end - -function wx_config_Private(wxRoot, wxDebug, wxHost, wxVersion, wxStatic, wxUnicode, wxUniversal, wxLibs, wxWindowsCompiler) - -- some options are not allowed for newer version of wxWidgets - if wxVersion > "2.8" then -- alphabetical comparison may fail... - -- wxDebug = "" -- 2.9 still make debug libraries - wxUnicode = "yes" - end - - --wx_root=PATH override wxRoot parameter - if _OPTIONS and _OPTIONS["wx_root"] then - print ("seen option '--wx_root=" .. _OPTIONS["wx_root"] .. "' overriding default root = '" .. wxRoot .. "'") - wxRoot = _OPTIONS["wx_root"] - end - -- the environment variable WXWIN override both wxRoot parameter and --wx_root option - if os.getenv('WXWIN') then wxRoot = os.getenv('WXWIN') end - - if wxUnicode == "yes" then - defines { "_UNICODE" } - characterset "Unicode" - end - - if wxDebug == "yes" then defines { "__WXDEBUG__" } - elseif wxDebug == "no" then optimize("On") end - - if wxStatic == "yes" then - --flags { "StaticRuntime" } - if os.target() == "windows" then - links { "ComCtl32", "RpcRT4", "WinMM", "AdvAPI32", "WSock32" } - end - else - defines { "WXUSINGDLL" } - end - - - -- function to compensate lack of wx-config program on windows - -- but wait, look at http://sites.google.com/site/wxconfig/ for one ! - function wx_config_for_windows(wxWindowsCompiler) - local wxBuildType = "" -- buildtype is one of "", "u", "d" or "ud" - local wxDebugSuffix = "" -- debug buildsuffix is for support libraries only - if wxUnicode ~= "no" then wxBuildType = wxBuildType .. "u" end - if wxDebug == "yes" then - wxBuildType = wxBuildType .. "d" - wxDebugSuffix = "d" - end - - local wxLibPath = path.join(wxRoot, "lib") - wxLibPath = path.join(wxLibPath, wxWindowsCompiler .. "_" .. iif(wxStatic == 'yes', 'lib', 'dll')) - -- common defines - defines{ "__WXMSW__" } - - -- common include path - includedirs { - path.join(wxRoot, "include"), - path.join(wxLibPath, "msw" .. wxBuildType) -- something like "%WXWIN%\lib\vc_lib\mswud" to find "wx/setup.h" - } - - -- common library path - libdirs { wxLibPath } - - -- add the libs - libVersion = string.gsub(wxVersion, '%.', '') -- remove dot from version - links { "wxbase"..libVersion..wxBuildType } -- base lib - for i, lib in ipairs(string.explode(wxLibs, ",")) do - local libPrefix = 'wxmsw' - if lib == "xml" or lib == "net" or lib == "odbc" then - libPrefix = 'wxbase' - end - links { libPrefix..libVersion..wxBuildType..'_'..lib} - end - -- link with support libraries - for i, lib in ipairs({"wxjpeg", "wxpng", "wxzlib", "wxtiff", "wxexpat"}) do - links { lib..wxDebugSuffix } - end - links { "wxregex" .. wxBuildType } - end - - -- use wx-config to figure out build parameters - function wx_config_for_posix() - local configCmd = "wx-config" -- this is the wx-config command line - if wxRoot ~= "" and os.target() ~= 'macosx' then configCmd = path.join(wxRoot, "bin/wx-config") end - - local function checkYesNo(value, option) - if value == "" then return "" end - if value == "yes" or value == "no" then return " --"..option.."="..value end - error("wx"..option..' can only be "yes", "no" or empty' ) - end - - configCmd = configCmd .. checkYesNo(wxDebug, "debug") - configCmd = configCmd .. checkYesNo(wxStatic, "static") - configCmd = configCmd .. checkYesNo(wxUnicode, "unicode") - configCmd = configCmd .. checkYesNo(wxUniversal, "universal") - if wxHost ~= "" then configCmd = configCmd .. " --host=" .. wxHost end - if wxVersion ~= "" then configCmd = configCmd .. " --version=" .. wxVersion end - - buildFlags = shellout(configCmd .." --cxxflags") - libFlags = shellout(configCmd .." --libs " .. wxLibs) - - if os.target() == 'macosx' then - -- on osx we need to concatenate the "-framework" and "Cocoa" parts, since they are split and it confuses GENie - for i, v in ipairs(libFlags) do - if v == '-framework' then - libFlags[i] = '-framework ' .. libFlags[i + 1] - libFlags[i + 1] = '' - end - end - end - - -- set the parameters to the curent configuration - buildoptions(buildFlags) - linkoptions(libFlags) - end - --- BUG: here, using any configuration() function will reset the current filter --- and apply configuration to all project configuration... --- see http://industriousone.com/post/add-way-refine-configuration-filter --- and http://sourceforge.net/tracker/?func=detail&aid=2936443&group_id=71616&atid=531881 ---~ configuration "not windows" ---~ wx_config_for_posix() ---~ configuration "vs*" ---~ wx_config_for_windows("vc") ---~ configuration {"windows", "codeblocks or gmake or codelitle"} ---~ wx_config_for_windows("gcc") - if os.target() ~= "windows" then - wx_config_for_posix() - else - local allowedCompiler = {"vc", "gcc"} - if not table.contains( allowedCompiler, wxWindowsCompiler) then - print( "wrong wxWidgets Compiler specified('"..wxWindowsCompiler.."'), should be one of '".. table.concat(allowedCompiler, "', '").."'" ) - wxWindowsCompiler = "vc" - end ---~ BUG/WISH: I need a function like compiler.get() that return the project/configuration compiler ---~ local wxWindowsCompiler = "vc" ---~ BUG? --cc=compiler standard premake option is not registered in the _OPTIONS array ---~ if _OPTIONS and _OPTIONS["cc"] then ---~ wxWindowsCompiler = _OPTIONS.cc ---~ print("seen option '--cc=" .. _OPTIONS["cc"] .. "' overriding default cc='vc'") ---~ end - wx_config_for_windows(wxWindowsCompiler) - end -end diff --git a/utils/cross_windows_dev_prepare.sh b/utils/cross_windows_dev_prepare.sh index 7da2b997f..33ed8815b 100755 --- a/utils/cross_windows_dev_prepare.sh +++ b/utils/cross_windows_dev_prepare.sh @@ -1,13 +1,10 @@ openal_ver="1.23.1" sdl2_ver="2.30.2" -wx_ver="3.2.1" slang_ver="2025.19.1" windows_openal_url="https://github.com/kcat/openal-soft/releases/download/${openal_ver}/openal-soft-${openal_ver}-bin.zip" windows_sdl2_url="https://github.com/libsdl-org/SDL/releases/download/release-${sdl2_ver}/SDL2-devel-${sdl2_ver}-VC.zip" -windows_wx_hdrs_url="https://github.com/wxWidgets/wxWidgets/releases/download/v${wx_ver}/wxWidgets-${wx_ver}-headers.7z" -windows_wx_libs_url="https://github.com/wxWidgets/wxWidgets/releases/download/v${wx_ver}/wxMSW-${wx_ver}_vc14x_x64_Dev.7z" windows_slang_url="https://github.com/shader-slang/slang/releases/download/v${slang_ver}/slang-${slang_ver}-windows-x86_64.zip" project_folder="$(pwd)" @@ -18,16 +15,12 @@ echo "Downloading dependencies..." # Download required dependencies wget $windows_sdl2_url -O "SDL2.zip" wget $windows_openal_url -O "OPENAL.zip" -wget $windows_wx_hdrs_url -O "WX_HDRS.7z" -wget $windows_wx_libs_url -O "WX_LIBS.7z" wget $windows_slang_url -O "SLANG.zip" echo "Extracting dependencies..." unzip "SDL2.zip" -d $dependency_folder unzip "OPENAL.zip" -d $dependency_folder -7z x "WX_HDRS.7z" -o"${dependency_folder}/wxWidgets" -7z x "WX_LIBS.7z" -o"${dependency_folder}/wxWidgets" unzip "SLANG.zip" -d "${dependency_folder}/slang" # Generate project files @@ -42,8 +35,6 @@ rm -rf $windows_sdl2_dir rm -f "SDL2.zip" rm -f "OPENAL.zip" -rm -f "WX_HDRS.7z" -rm -f "WX_LIBS.7z" rm -f "SLANG.zip" echo "Completed" diff --git a/utils/linux_dev_prepare.sh b/utils/linux_dev_prepare.sh index 66aeb9bbf..334ef54ac 100755 --- a/utils/linux_dev_prepare.sh +++ b/utils/linux_dev_prepare.sh @@ -5,7 +5,7 @@ set -x SLANG_VER="2025.19.1" DAV1D_VER="1.5.4" -# TODO: openal-soft, SDL2, X11, Wayland, wxGTK packages +# TODO: openal-soft, SDL2, X11, Wayland packages wget "https://github.com/shader-slang/slang/releases/download/v${SLANG_VER}/slang-${SLANG_VER}-linux-x86_64.tar.gz" tar -xzf ./slang-${SLANG_VER}-linux-x86_64.tar.gz -C "./src_dependency/slang/" diff --git a/utils/win_install_7zip4powershell.ps1 b/utils/win_install_7zip4powershell.ps1 deleted file mode 100644 index 81ceb69d1..000000000 --- a/utils/win_install_7zip4powershell.ps1 +++ /dev/null @@ -1,4 +0,0 @@ -[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -Install-PackageProvider -Name NuGet -Scope CurrentUser -MinimumVersion 2.8.5.201 -Force -Set-PSRepository -Name 'PSGallery' -SourceLocation "https://www.powershellgallery.com/api/v2" -InstallationPolicy Trusted -Install-Module -Name 7Zip4PowerShell -Force \ No newline at end of file diff --git a/utils/windows_dev_prepare.ps1 b/utils/windows_dev_prepare.ps1 index cb69829b1..6d8f7e8bb 100644 --- a/utils/windows_dev_prepare.ps1 +++ b/utils/windows_dev_prepare.ps1 @@ -1,14 +1,11 @@ $openal_ver = '1.23.1' $sdl2_ver = '2.30.2' -$wx_ver = '3.2.1' $slang_ver = '2025.19.1' $windows_openal_url = 'https://github.com/kcat/openal-soft/releases/download/' + $openal_ver + '/openal-soft-' + $openal_ver + '-bin.zip' $windows_sdl2_url = 'https://github.com/libsdl-org/SDL/releases/download/release-' + $sdl2_ver + '/SDL2-devel-' + $sdl2_ver + '-VC.zip' -$windows_wx_hdrs_url = 'https://github.com/wxWidgets/wxWidgets/releases/download/v' +$wx_ver+ '/wxWidgets-' +$wx_ver+ '-headers.7z' -$windows_wx_libs_url = 'https://github.com/wxWidgets/wxWidgets/releases/download/v' +$wx_ver+ '/wxMSW-' +$wx_ver+ '_vc14x_x64_Dev.7z' $windows_slang_url = 'https://github.com/shader-slang/slang/releases/download/v' +$slang_ver+ '/slang-' +$slang_ver+ '-windows-x86_64.zip' $project_folder = '.\\' @@ -17,14 +14,10 @@ $dependency_folder = $project_folder + '\\src_dependency' # Download required dependencies Invoke-WebRequest -Uri $windows_sdl2_url -OutFile SDL2.zip Invoke-WebRequest -Uri $windows_openal_url -OutFile OPENAL.zip -Invoke-WebRequest -Uri $windows_wx_hdrs_url -OutFile WX_HDRS.7z -Invoke-WebRequest -Uri $windows_wx_libs_url -OutFile WX_LIBS.7z Invoke-WebRequest -Uri $windows_slang_url -OutFile SLANG.zip Expand-Archive SDL2.zip -DestinationPath $dependency_folder Expand-Archive OPENAL.zip -DestinationPath $dependency_folder -Expand-7Zip -ArchiveFileName WX_HDRS.7z -TargetPath ($dependency_folder + '\\wxWidgets') -Expand-7Zip -ArchiveFileName WX_LIBS.7z -TargetPath ($dependency_folder + '\\wxWidgets') Expand-Archive SLANG.zip -DestinationPath ($dependency_folder + '\\slang') # Generate project files @@ -39,8 +32,6 @@ Remove-Item $windows_sdl2_dir -Recurse Remove-Item SDL2.zip Remove-Item OPENAL.zip -Remove-Item WX_HDRS.7z -Remove-Item WX_LIBS.7z Remove-Item SLANG.zip Set-Location -Path $project_folder