Probe for version-specific types at startup to determine which MapEnhancer build is installed, then gray out features unavailable in that build. Three tiers: official v1.5.2 (minimal), v1.6.0 (adds turntable markers), and the community fork (full feature set). Detection uses type probes - TurntableHelper for the v1.6.0+ tier, SwitchResetAuditState for community. Adds HasTurntable and IsCommunity capability flags on MapEnhancerBridge, imMEHasTurntable/imMECommunity atomics on PopoutWindow, and a two-arg RRPOPOUT_SetMapEnhancerCaps export. The renderer reads both flags and wraps community-only menu items in BeginDisabled blocks accordingly. Updates README with version compatibility notes and comparison screenshots.
1248 lines
58 KiB
C++
1248 lines
58 KiB
C++
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#include <Windows.h>
|
|
#include <d3d11.h>
|
|
#include <dxgi.h>
|
|
#include <d3dcompiler.h>
|
|
#include <wrl/client.h>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include "d3d11_renderer.h"
|
|
#include "popout_window.h"
|
|
#include "popout_windows.h"
|
|
#include "../include/shared_types.h"
|
|
|
|
// Dear ImGui + D3D11 backend
|
|
#include "imgui.h"
|
|
#include "imgui_impl_dx11.h"
|
|
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared device/context captured from Unity via UnityPluginLoad
|
|
// ---------------------------------------------------------------------------
|
|
static ID3D11Device* g_device = nullptr;
|
|
static ID3D11DeviceContext* g_context = nullptr;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared blit resources (created once in Renderer_OnDeviceInit)
|
|
// ---------------------------------------------------------------------------
|
|
static ComPtr<ID3D11VertexShader> g_vs;
|
|
static ComPtr<ID3D11PixelShader> g_ps;
|
|
static ComPtr<ID3D11Buffer> g_uvRectCB;
|
|
static ComPtr<ID3D11SamplerState> g_sampler;
|
|
static ComPtr<ID3D11BlendState> g_blendOpaque;
|
|
static ComPtr<ID3D11RasterizerState> g_rasterState;
|
|
static ComPtr<ID3D11DepthStencilState> g_depthState;
|
|
|
|
static bool g_imguiInited = false;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme state — written by Renderer_SetTheme (main thread), applied on the
|
|
// render thread at frame start. Mutex protects the struct copy; g_themeChanged
|
|
// signals the render thread to call ApplyThemeToStyle before NewFrame.
|
|
// ---------------------------------------------------------------------------
|
|
static MapThemeData g_theme;
|
|
static std::mutex g_themeMutex;
|
|
static std::atomic<bool> g_themeChanged {false};
|
|
static std::atomic<int> g_currentThemePreset {0};
|
|
static std::atomic<float> g_ovAlpha {1.0f}; // chrome (window + toolbar + compass) alpha
|
|
static std::atomic<float> g_mapAlpha {1.0f}; // map Image() alpha, independent of chrome
|
|
static std::atomic<float> g_mapBgAlpha {1.0f}; // camera clear colour opacity
|
|
|
|
// cbuffer layout — must be 16-byte aligned
|
|
struct alignas(16) UVRectCB { float u0, v0, u1, v1; };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HLSL: fullscreen triangle blit
|
|
// ---------------------------------------------------------------------------
|
|
static const char* kBlitVS = R"hlsl(
|
|
cbuffer UVRect : register(b0) { float4 _uvRect; };
|
|
struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
|
|
VSOut main(uint id : SV_VertexID) {
|
|
VSOut o;
|
|
float2 base = float2((id & 2u) ? 2.f : 0.f, (id & 1u) ? 2.f : 0.f);
|
|
o.pos = float4(base.x * 2.f - 1.f, 1.f - base.y * 2.f, 0.f, 1.f);
|
|
o.uv = lerp(_uvRect.xy, _uvRect.zw, base);
|
|
return o;
|
|
}
|
|
)hlsl";
|
|
|
|
static const char* kBlitPS = R"hlsl(
|
|
Texture2D gTex : register(t0);
|
|
SamplerState gSamp : register(s0);
|
|
struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
|
|
float4 main(VSOut i) : SV_Target { return gTex.Sample(gSamp, i.uv); }
|
|
)hlsl";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
static DXGI_FORMAT TypedFormat(DXGI_FORMAT f) {
|
|
switch (f) {
|
|
case DXGI_FORMAT_R8G8B8A8_TYPELESS: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
case DXGI_FORMAT_B8G8R8A8_TYPELESS: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
|
case DXGI_FORMAT_R16G16B16A16_TYPELESS: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
|
case DXGI_FORMAT_R32G32B32A32_TYPELESS: return DXGI_FORMAT_R32G32B32A32_FLOAT;
|
|
case DXGI_FORMAT_R10G10B10A2_TYPELESS: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
|
case DXGI_FORMAT_R32G32_TYPELESS: return DXGI_FORMAT_R32G32_FLOAT;
|
|
default: return f;
|
|
}
|
|
}
|
|
|
|
// Non-sRGB (linear) render-target view format for a backbuffer, so ImGui's writes
|
|
// are NOT sRGB-encoded by the GPU (which brightens). Returns UNKNOWN for formats
|
|
// where no such fix is needed/possible (e.g. float HDR has no encode anyway).
|
|
static DXGI_FORMAT UnormRtvFormat(DXGI_FORMAT f) {
|
|
switch (f) {
|
|
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
case DXGI_FORMAT_B8G8R8A8_TYPELESS:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
|
case DXGI_FORMAT_R10G10B10A2_TYPELESS:
|
|
case DXGI_FORMAT_R10G10B10A2_UNORM: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
|
default: return DXGI_FORMAT_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// D3D11 state save/restore — prevents corrupting Unity's render state
|
|
// ---------------------------------------------------------------------------
|
|
struct D3D11StateBlock {
|
|
ComPtr<ID3D11RenderTargetView> rtv[8];
|
|
ComPtr<ID3D11DepthStencilView> dsv;
|
|
UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
|
|
D3D11_VIEWPORT viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
|
ComPtr<ID3D11VertexShader> vs;
|
|
ComPtr<ID3D11PixelShader> ps;
|
|
ComPtr<ID3D11Buffer> vsCB;
|
|
ComPtr<ID3D11InputLayout> inputLayout;
|
|
D3D11_PRIMITIVE_TOPOLOGY topology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED;
|
|
ComPtr<ID3D11ShaderResourceView> psSRV[1];
|
|
ComPtr<ID3D11SamplerState> psSampler[1];
|
|
ComPtr<ID3D11BlendState> blendState;
|
|
FLOAT blendFactor[4];
|
|
UINT sampleMask;
|
|
ComPtr<ID3D11RasterizerState> rasterizerState;
|
|
ComPtr<ID3D11DepthStencilState> depthStencilState;
|
|
UINT stencilRef;
|
|
|
|
void Capture(ID3D11DeviceContext* ctx) {
|
|
ctx->OMGetRenderTargets(8, &rtv[0], &dsv);
|
|
ctx->RSGetViewports(&numViewports, viewports);
|
|
ctx->VSGetShader(&vs, nullptr, nullptr);
|
|
ctx->VSGetConstantBuffers(0, 1, &vsCB);
|
|
ctx->PSGetShader(&ps, nullptr, nullptr);
|
|
ctx->IAGetInputLayout(&inputLayout);
|
|
ctx->IAGetPrimitiveTopology(&topology);
|
|
ctx->PSGetShaderResources(0, 1, &psSRV[0]);
|
|
ctx->PSGetSamplers(0, 1, &psSampler[0]);
|
|
ctx->OMGetBlendState(&blendState, blendFactor, &sampleMask);
|
|
ctx->RSGetState(&rasterizerState);
|
|
ctx->OMGetDepthStencilState(&depthStencilState, &stencilRef);
|
|
}
|
|
void Restore(ID3D11DeviceContext* ctx) {
|
|
ID3D11RenderTargetView* rtvPtrs[8];
|
|
for (int i = 0; i < 8; i++) rtvPtrs[i] = rtv[i].Get();
|
|
ctx->OMSetRenderTargets(8, rtvPtrs, dsv.Get());
|
|
ctx->RSSetViewports(numViewports, viewports);
|
|
ctx->VSSetShader(vs.Get(), nullptr, 0);
|
|
ID3D11Buffer* cb = vsCB.Get();
|
|
ctx->VSSetConstantBuffers(0, 1, &cb);
|
|
ctx->PSSetShader(ps.Get(), nullptr, 0);
|
|
ctx->IASetInputLayout(inputLayout.Get());
|
|
ctx->IASetPrimitiveTopology(topology);
|
|
ctx->PSSetShaderResources(0, 1, &psSRV[0]);
|
|
ctx->PSSetSamplers(0, 1, &psSampler[0]);
|
|
ctx->OMSetBlendState(blendState.Get(), blendFactor, sampleMask);
|
|
ctx->RSSetState(rasterizerState.Get());
|
|
ctx->OMSetDepthStencilState(depthStencilState.Get(), stencilRef);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme helpers — render-thread only
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Apply g_theme to ImGui's style. Safe after ImGui::CreateContext(); does not need
|
|
// the DX11 backend. Called on the render thread before NewFrame, or from InitImGui.
|
|
static void ApplyThemeToStyle(const MapThemeData& t) {
|
|
ImGuiStyle& s = ImGui::GetStyle();
|
|
|
|
auto iv4 = [](float r, float g, float b, float a) { return ImVec4(r, g, b, a); };
|
|
auto darken = [](ImVec4 c, float f) {
|
|
return ImVec4(c.x*f, c.y*f, c.z*f, c.w);
|
|
};
|
|
auto brighten = [](ImVec4 c, float f) {
|
|
return ImVec4(std::min(1.f, c.x*f), std::min(1.f, c.y*f), std::min(1.f, c.z*f), c.w);
|
|
};
|
|
|
|
ImVec4 bg = iv4(t.wBgR, t.wBgG, t.wBgB, t.wBgA);
|
|
ImVec4 acc = iv4(t.accR, t.accG, t.accB, t.accA);
|
|
ImVec4 txt = iv4(t.txtR, t.txtG, t.txtB, t.txtA);
|
|
ImVec4 pop = iv4(t.popR, t.popG, t.popB, t.popA);
|
|
|
|
s.Colors[ImGuiCol_WindowBg] = bg;
|
|
s.Colors[ImGuiCol_ChildBg] = bg;
|
|
s.Colors[ImGuiCol_PopupBg] = pop;
|
|
s.Colors[ImGuiCol_Text] = txt;
|
|
s.Colors[ImGuiCol_TextDisabled] = darken(txt, 0.5f);
|
|
|
|
s.Colors[ImGuiCol_TitleBgActive] = acc;
|
|
s.Colors[ImGuiCol_TitleBg] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_TitleBgCollapsed] = darken(acc, 0.35f);
|
|
|
|
s.Colors[ImGuiCol_Button] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_ButtonHovered] = darken(acc, 0.80f);
|
|
s.Colors[ImGuiCol_ButtonActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_Header] = darken(acc, 0.40f);
|
|
s.Colors[ImGuiCol_HeaderHovered] = darken(acc, 0.65f);
|
|
s.Colors[ImGuiCol_HeaderActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_FrameBg] = iv4(t.wBgR+0.07f, t.wBgG+0.07f, t.wBgB+0.07f, t.wBgA);
|
|
s.Colors[ImGuiCol_FrameBgHovered] = darken(acc, 0.35f);
|
|
s.Colors[ImGuiCol_FrameBgActive] = darken(acc, 0.55f);
|
|
|
|
s.Colors[ImGuiCol_SliderGrab] = brighten(acc, 1.1f);
|
|
s.Colors[ImGuiCol_SliderGrabActive] = brighten(acc, 1.4f);
|
|
s.Colors[ImGuiCol_CheckMark] = brighten(acc, 1.2f);
|
|
|
|
s.Colors[ImGuiCol_ResizeGrip] = iv4(t.accR, t.accG, t.accB, 0.20f);
|
|
s.Colors[ImGuiCol_ResizeGripHovered] = iv4(t.accR, t.accG, t.accB, 0.67f);
|
|
s.Colors[ImGuiCol_ResizeGripActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_Separator] = iv4(t.accR, t.accG, t.accB, 0.35f);
|
|
s.Colors[ImGuiCol_SeparatorHovered] = darken(acc, 0.7f);
|
|
s.Colors[ImGuiCol_SeparatorActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_MenuBarBg] = darken(bg, 0.85f);
|
|
s.Colors[ImGuiCol_ScrollbarBg] = darken(bg, 0.70f);
|
|
s.Colors[ImGuiCol_ScrollbarGrab] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_ScrollbarGrabHovered] = darken(acc, 0.75f);
|
|
s.Colors[ImGuiCol_ScrollbarGrabActive] = acc;
|
|
}
|
|
|
|
// Snapshots g_theme under a single mutex lock, applying a pending style change first
|
|
// if g_themeChanged is set. Replaces the previous double-lock pattern (lock to apply,
|
|
// then lock again to copy). Call once at the top of each render entry point.
|
|
static MapThemeData SnapshotTheme() {
|
|
bool changed = g_themeChanged.exchange(false);
|
|
std::lock_guard<std::mutex> lk(g_themeMutex);
|
|
if (changed) ApplyThemeToStyle(g_theme);
|
|
return g_theme;
|
|
}
|
|
|
|
// Default S3 Dark theme — identical to what InitImGui used to hardcode.
|
|
static const MapThemeData kS3DarkTheme = {
|
|
0.12f, 0.12f, 0.12f, 1.00f, // windowBg
|
|
0.26f, 0.59f, 0.98f, 1.00f, // accent (ImGui default TitleBgActive blue)
|
|
1.00f, 1.00f, 1.00f, 1.00f, // text
|
|
0.08f, 0.08f, 0.08f, 0.94f, // popupBg
|
|
1.00f, 1.00f, 1.00f, 1.00f, // mapTint (no tint)
|
|
0.086f,0.086f,0.086f,0.784f, // compassBg (IM_COL32(22,22,22,200))
|
|
0.804f,0.196f,0.196f,1.00f, // needleN (IM_COL32(205,50,50,255))
|
|
0.804f,0.804f,0.804f,0.863f, // needleS (IM_COL32(205,205,205,220))
|
|
0.082f,0.122f,0.157f,1.00f, // mapBg camera clear (dark blue-gray)
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ImGui initialisation (call once per D3D11 device)
|
|
// ---------------------------------------------------------------------------
|
|
static void InitImGui(ID3D11Device* device, ID3D11DeviceContext* context) {
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.IniFilename = nullptr; // no imgui.ini — we're a mod, not a standalone app
|
|
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // Win32 owns the cursor
|
|
|
|
// Dark theme base — specific colours are overridden by ApplyThemeToStyle below.
|
|
ImGui::StyleColorsDark();
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.WindowRounding = 0.f;
|
|
style.WindowBorderSize = 0.f;
|
|
// Allow the 22px toolbar to be exactly 22px. The default 32px minimum makes it
|
|
// overhang the bottom of the in-game map window as a stray dark strip.
|
|
style.WindowMinSize = ImVec2(4.f, 4.f);
|
|
style.FrameRounding = 3.f;
|
|
style.GrabRounding = 3.f;
|
|
style.ScrollbarRounding = 3.f;
|
|
|
|
// Apply the current theme (or the default S3 Dark if none has been pushed yet).
|
|
{ std::lock_guard<std::mutex> lk(g_themeMutex); if (g_theme.accA == 0.f) g_theme = kS3DarkTheme; }
|
|
ApplyThemeToStyle(g_theme);
|
|
|
|
// Fonts: default (ASCII) + merge the ⚙ gear glyph from Segoe UI Symbol.
|
|
// If seguisym.ttf is absent the button renders a box — fully functional fallback.
|
|
io.Fonts->AddFontDefault();
|
|
{
|
|
ImFontConfig fc;
|
|
fc.MergeMode = true;
|
|
fc.GlyphMinAdvanceX = 13.f;
|
|
static const ImWchar kGlyphRanges[] = {
|
|
0x2699, 0x2699, // ⚙ gear (settings button)
|
|
0x25CE, 0x25CE, // ◎ bullseye (follow-player button)
|
|
0 };
|
|
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisym.ttf",
|
|
14.f, &fc, kGlyphRanges);
|
|
// seguisym.ttf absent → AddFontFromFileTTF returns nullptr, merge is skipped
|
|
}
|
|
io.Fonts->Build();
|
|
|
|
ImGui_ImplDX11_Init(device, context);
|
|
g_imguiInited = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
void Renderer_OnDeviceInit(ID3D11Device* device) {
|
|
if (!device) return;
|
|
g_device = device;
|
|
device->GetImmediateContext(&g_context);
|
|
|
|
// Compile blit shaders
|
|
ComPtr<ID3DBlob> vsBlob, psBlob, errBlob;
|
|
if (FAILED(D3DCompile(kBlitVS, strlen(kBlitVS), "BlitVS", nullptr, nullptr,
|
|
"main", "vs_5_0", 0, 0, &vsBlob, &errBlob))) {
|
|
if (errBlob) OutputDebugStringA((char*)errBlob->GetBufferPointer());
|
|
return;
|
|
}
|
|
if (FAILED(D3DCompile(kBlitPS, strlen(kBlitPS), "BlitPS", nullptr, nullptr,
|
|
"main", "ps_5_0", 0, 0, &psBlob, &errBlob))) {
|
|
if (errBlob) OutputDebugStringA((char*)errBlob->GetBufferPointer());
|
|
return;
|
|
}
|
|
device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &g_vs);
|
|
device->CreatePixelShader (psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &g_ps);
|
|
|
|
D3D11_SAMPLER_DESC sd = {};
|
|
sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
|
sd.AddressU = sd.AddressV = sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
|
sd.MaxLOD = D3D11_FLOAT32_MAX;
|
|
device->CreateSamplerState(&sd, &g_sampler);
|
|
|
|
D3D11_BLEND_DESC bd = {};
|
|
bd.RenderTarget[0].BlendEnable = FALSE;
|
|
bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
|
device->CreateBlendState(&bd, &g_blendOpaque);
|
|
|
|
D3D11_RASTERIZER_DESC rd = {};
|
|
rd.FillMode = D3D11_FILL_SOLID;
|
|
rd.CullMode = D3D11_CULL_NONE;
|
|
device->CreateRasterizerState(&rd, &g_rasterState);
|
|
|
|
D3D11_DEPTH_STENCIL_DESC dd = {};
|
|
dd.DepthEnable = FALSE;
|
|
device->CreateDepthStencilState(&dd, &g_depthState);
|
|
|
|
D3D11_BUFFER_DESC cbd = {};
|
|
cbd.ByteWidth = sizeof(UVRectCB);
|
|
cbd.Usage = D3D11_USAGE_DYNAMIC;
|
|
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
|
|
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
|
device->CreateBuffer(&cbd, nullptr, &g_uvRectCB);
|
|
|
|
InitImGui(device, g_context);
|
|
}
|
|
|
|
void Renderer_SetTheme(const MapThemeData* t, int presetIndex) {
|
|
if (!t) return;
|
|
{ std::lock_guard<std::mutex> lk(g_themeMutex); g_theme = *t; }
|
|
g_currentThemePreset.store(presetIndex);
|
|
g_themeChanged.store(true);
|
|
}
|
|
|
|
void Renderer_OnDeviceShutdown() {
|
|
if (g_imguiInited) {
|
|
ImGui_ImplDX11_Shutdown();
|
|
ImGui::DestroyContext();
|
|
g_imguiInited = false;
|
|
}
|
|
g_vs.Reset(); g_ps.Reset(); g_uvRectCB.Reset();
|
|
g_sampler.Reset(); g_blendOpaque.Reset();
|
|
g_rasterState.Reset(); g_depthState.Reset();
|
|
if (g_context) { g_context->Release(); g_context = nullptr; }
|
|
g_device = nullptr;
|
|
}
|
|
|
|
bool Renderer_EnsureSwapchain(PopoutWindow* win) {
|
|
if (!g_device || !win || !win->contentHwnd) return false;
|
|
int w = win->width.load(), h = win->height.load();
|
|
if (w <= 0 || h <= 0) return false;
|
|
|
|
if (win->swapChain) {
|
|
DXGI_SWAP_CHAIN_DESC desc;
|
|
win->swapChain->GetDesc(&desc);
|
|
if ((int)desc.BufferDesc.Width == w && (int)desc.BufferDesc.Height == h) return true;
|
|
g_context->OMSetRenderTargets(0, nullptr, nullptr);
|
|
g_context->Flush();
|
|
if (win->rtv) { win->rtv->Release(); win->rtv = nullptr; }
|
|
if (win->swapChain) { win->swapChain->Release(); win->swapChain = nullptr; }
|
|
}
|
|
|
|
ComPtr<IDXGIDevice> dxgiDevice; ComPtr<IDXGIAdapter> adapter; ComPtr<IDXGIFactory> factory;
|
|
if (FAILED(g_device->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice))) return false;
|
|
if (FAILED(dxgiDevice->GetAdapter(&adapter))) return false;
|
|
if (FAILED(adapter->GetParent(__uuidof(IDXGIFactory), (void**)&factory))) return false;
|
|
|
|
DXGI_SWAP_CHAIN_DESC scd = {};
|
|
scd.BufferCount = 2;
|
|
scd.BufferDesc.Width = static_cast<UINT>(w);
|
|
scd.BufferDesc.Height = static_cast<UINT>(h);
|
|
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
|
scd.OutputWindow = win->contentHwnd;
|
|
scd.SampleDesc.Count = 1;
|
|
scd.Windowed = TRUE;
|
|
scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
|
|
scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
|
|
if (FAILED(factory->CreateSwapChain(g_device, &scd, &win->swapChain))) return false;
|
|
factory->MakeWindowAssociation(win->contentHwnd, DXGI_MWA_NO_ALT_ENTER);
|
|
|
|
ID3D11Texture2D* bb = nullptr;
|
|
win->swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&bb);
|
|
if (bb) { g_device->CreateRenderTargetView(bb, nullptr, &win->rtv); bb->Release(); }
|
|
return win->rtv != nullptr;
|
|
}
|
|
|
|
void Renderer_ReleaseSwapchain(PopoutWindow* win) {
|
|
if (!win) return;
|
|
if (win->rtv) { win->rtv->Release(); win->rtv = nullptr; }
|
|
if (win->swapChain) { win->swapChain->Release(); win->swapChain = nullptr; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared map UI chrome (compass rose + bottom toolbar), drawn for window `win`
|
|
// at logical size w x h. Pure ImGui: reads win-> state atomics and pushes
|
|
// UICommands onto win->inputQueue. The caller owns the frame lifecycle — it must
|
|
// set up io, call ImGui_ImplDX11_NewFrame()/ImGui::NewFrame() before this, and
|
|
// ImGui::Render() after. Used by the popout (Renderer_Present) and, in time, by
|
|
// the in-game overlay so the map UI is authored once and rendered on any surface.
|
|
// ---------------------------------------------------------------------------
|
|
// origin = top-left screen position of the map region; w,h = its size. For the
|
|
// popout the region is the whole window (origin 0,0); for the in-game overlay it
|
|
// is the floating window's map image rect, so the chrome tracks that window.
|
|
// reserveResizeGrip: shorten the toolbar so the in-game ImGui window's bottom-right
|
|
// resize grip stays grabbable (the popout uses its OS window border instead).
|
|
static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|
bool reserveResizeGrip, bool showPopOutBtn,
|
|
const MapThemeData& theme, float mapAlpha = 1.0f) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Shared command helper — usable from any window in this frame
|
|
auto pushCmd = [&](UICmd cmd, float idx = 0.f) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(cmd); ev.y = idx;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
|
|
const float kBarH = 22.f;
|
|
|
|
// ── Compass overlay (floating in the bottom-right of the map area) ──
|
|
const float kCR = 36.f; // compass circle radius
|
|
const float kCWin = kCR * 2.f + 20.f; // window size (leaves room for N label)
|
|
const float kCPad = 10.f; // margin from window edge
|
|
|
|
if (h > kBarH + kCWin + kCPad * 2.f) { // skip if window too small
|
|
ImGui::SetNextWindowPos(
|
|
ImVec2(origin.x + w - kCWin - kCPad, origin.y + h - kBarH - kCWin - kCPad),
|
|
ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(kCWin, kCWin));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
|
ImGui::Begin("##compass_overlay", nullptr,
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBackground);
|
|
|
|
ImGui::SetCursorPos(ImVec2(0.f, 0.f));
|
|
ImGui::InvisibleButton("##compassHit", ImVec2(kCWin, kCWin));
|
|
bool cHov = ImGui::IsItemHovered();
|
|
bool cAct = ImGui::IsItemActive();
|
|
|
|
// Drag to rotate: angle from compass centre → new bearing
|
|
if (cAct) {
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f };
|
|
float ddx = io.MousePos.x - cc.x, ddy = io.MousePos.y - cc.y;
|
|
if (ddx*ddx + ddy*ddy > 9.f) {
|
|
float ang = atan2f(ddx, -ddy) * (180.f / 3.14159265f);
|
|
if (ang < 0.f) ang += 360.f;
|
|
win->imMapRotationDeg.store(ang);
|
|
win->imMapSyncPlayer.store(false);
|
|
pushCmd(UICmd::SetRotation, ang);
|
|
}
|
|
}
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
win->imMapRotationDeg.store(0.f);
|
|
win->imMapSyncPlayer.store(false);
|
|
pushCmd(UICmd::RotateReset);
|
|
}
|
|
|
|
// Draw compass rose
|
|
{
|
|
float rotDeg = win->imMapRotationDeg.load();
|
|
float rotRad = rotDeg * (3.14159265f / 180.f);
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f };
|
|
|
|
// Derive compass colours from theme; scale alpha by mapAlpha so the
|
|
// compass responds to the Map Elements opacity slider.
|
|
auto col32 = [&](float r, float g, float b, float a) -> ImU32 {
|
|
return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a * mapAlpha * 255));
|
|
};
|
|
ImU32 bgCol = cAct ? col32(theme.cmpR*1.6f, theme.cmpG*1.6f, theme.cmpB*1.6f, theme.cmpA)
|
|
: cHov ? col32(theme.cmpR*1.3f, theme.cmpG*1.3f, theme.cmpB*1.3f, theme.cmpA)
|
|
: col32(theme.cmpR, theme.cmpG, theme.cmpB, theme.cmpA);
|
|
ImU32 ringCol = col32(theme.accR*0.5f, theme.accG*0.5f, theme.accB*0.5f, 0.78f);
|
|
ImU32 tickCard = col32(theme.txtR*0.67f, theme.txtG*0.67f, theme.txtB*0.67f, 0.86f);
|
|
ImU32 tickIter = col32(theme.txtR*0.50f, theme.txtG*0.50f, theme.txtB*0.50f, 0.51f);
|
|
ImU32 needleN = col32(theme.nNR, theme.nNG, theme.nNB, theme.nNA);
|
|
ImU32 needleS = col32(theme.nSR, theme.nSG, theme.nSB, theme.nSA);
|
|
ImU32 labelN = col32(std::min(1.f,theme.nNR*1.1f), std::min(1.f,theme.nNG*1.1f),
|
|
std::min(1.f,theme.nNB*1.1f), 1.0f);
|
|
ImU32 capOuter = col32(theme.cmpR*0.85f, theme.cmpG*0.85f, theme.cmpB*0.85f, 1.0f);
|
|
ImU32 capInner = col32(theme.txtR*0.82f, theme.txtG*0.82f, theme.txtB*0.82f, 1.0f);
|
|
|
|
// Background disk
|
|
dl->AddCircleFilled(cc, kCR + 2.f, bgCol, 48);
|
|
dl->AddCircle(cc, kCR + 2.f, ringCol, 48, 1.5f);
|
|
|
|
// Tick marks: 8 directions — cardinals longer, inter-cardinals shorter
|
|
for (int i = 0; i < 8; ++i) {
|
|
float a = (i * 45.f - rotDeg) * (3.14159265f / 180.f);
|
|
float ox = sinf(a), oy = -cosf(a);
|
|
bool card = (i % 2 == 0);
|
|
float inner = kCR - (card ? 9.f : 5.f);
|
|
dl->AddLine({ cc.x + ox*inner, cc.y + oy*inner },
|
|
{ cc.x + ox*(kCR - 1.f), cc.y + oy*(kCR - 1.f) },
|
|
card ? tickCard : tickIter,
|
|
card ? 1.5f : 1.f);
|
|
}
|
|
|
|
// Needle: diamond split N / S colours from theme
|
|
float nx = sinf(rotRad), ny = -cosf(rotRad);
|
|
float bx = -ny * 7.f, by = nx * 7.f;
|
|
ImVec2 nTip = { cc.x + nx*(kCR-5.f), cc.y + ny*(kCR-5.f) };
|
|
ImVec2 sTail = { cc.x - nx*(kCR-5.f)*0.55f, cc.y - ny*(kCR-5.f)*0.55f };
|
|
ImVec2 bleft = { cc.x + bx, cc.y + by };
|
|
ImVec2 brght = { cc.x - bx, cc.y - by };
|
|
|
|
dl->AddTriangleFilled(nTip, bleft, brght, needleN);
|
|
dl->AddTriangleFilled(sTail, bleft, brght, needleS);
|
|
dl->AddTriangle(nTip, bleft, brght, IM_COL32(0,0,0,100), 0.5f);
|
|
dl->AddTriangle(sTail, bleft, brght, IM_COL32(0,0,0, 80), 0.5f);
|
|
|
|
// Centre cap
|
|
dl->AddCircleFilled(cc, 5.5f, capOuter);
|
|
dl->AddCircleFilled(cc, 3.5f, capInner);
|
|
|
|
// "N" label at needle tip
|
|
ImVec2 nPos = { cc.x + nx*(kCR + 4.f) - 4.f, cc.y + ny*(kCR + 4.f) - 7.f };
|
|
dl->AddText(ImGui::GetFont(), 14.f, nPos, labelN, "N");
|
|
}
|
|
|
|
if (cHov)
|
|
ImGui::SetTooltip("Rotation: %.0f\xc2\xb0\nDrag to rotate | Right-click to reset",
|
|
win->imMapRotationDeg.load());
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(2);
|
|
}
|
|
|
|
// ── Toolbar strip ────────────────────────────────────────────────────
|
|
// Leave the bottom-right corner clear for the ImGui resize grip when in-game.
|
|
const float kGripReserve = reserveResizeGrip ? 18.f : 0.f;
|
|
ImGui::SetNextWindowPos(ImVec2(origin.x, origin.y + (float)h - kBarH));
|
|
ImGui::SetNextWindowSize(ImVec2((float)w - kGripReserve, kBarH));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 2.f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 2.f));
|
|
ImGui::Begin("##toolbar", nullptr,
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoFocusOnAppearing);
|
|
|
|
// Status label (left)
|
|
{
|
|
char buf[256];
|
|
{ std::lock_guard<std::mutex> lk(win->imStatusMutex); strncpy_s(buf, win->imStatusText, _TRUNCATE); }
|
|
ImGui::TextUnformatted(buf);
|
|
}
|
|
|
|
const float kBtnH = kBarH - 4.f;
|
|
const float kBtnW = 22.f;
|
|
const float kGap = 4.f;
|
|
|
|
// Toolbar buttons use the accent colour from the current theme. Active (lit)
|
|
// state is a 40% brighter version of the accent.
|
|
const ImVec4 kBtnBlue = ImGui::GetStyleColorVec4(ImGuiCol_TitleBgActive);
|
|
const ImVec4 kBtnBlueActive = ImVec4(
|
|
std::min(1.f, kBtnBlue.x * 1.4f),
|
|
std::min(1.f, kBtnBlue.y * 1.4f),
|
|
std::min(1.f, kBtnBlue.z * 1.4f), 1.f);
|
|
|
|
// Luminance-based contrast text: same formula as Railroader's ColorHelper.IsDark()
|
|
auto contrastText = [](ImVec4 bg) -> ImVec4 {
|
|
float L = 0.299f*bg.x + 0.587f*bg.y + 0.114f*bg.z;
|
|
return L > 0.45f ? ImVec4(0.10f, 0.10f, 0.10f, 1.f)
|
|
: ImVec4(0.95f, 0.95f, 0.95f, 1.f);
|
|
};
|
|
|
|
// >> Pop Out (in-game overlay only) — switches map to the OS popout window
|
|
if (showPopOutBtn) {
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW - kGap - kBtnW - kGap - kBtnW);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, kBtnBlue);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(kBtnBlue));
|
|
if (ImGui::Button(">>##popout", ImVec2(kBtnW, kBtnH)))
|
|
pushCmd(UICmd::PopOut);
|
|
ImGui::PopStyleColor(2);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("Open in popout window");
|
|
}
|
|
|
|
// ◎ Follow-player position toggle
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW - kGap - kBtnW);
|
|
{
|
|
bool fOn = win->imFollowPlayer.load();
|
|
ImVec4 fColor = fOn ? kBtnBlueActive : kBtnBlue;
|
|
ImGui::PushStyleColor(ImGuiCol_Button, fColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(fColor));
|
|
if (ImGui::Button("\xe2\x97\x8e##follow", ImVec2(kBtnW, kBtnH)))
|
|
pushCmd(UICmd::ToggleFollowPlayer);
|
|
ImGui::PopStyleColor(2);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(fOn ? "Following player position\nClick to stop"
|
|
: "Follow player position");
|
|
}
|
|
|
|
// ⚙ Settings gear
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, kBtnBlue);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(kBtnBlue));
|
|
if (ImGui::Button("\xe2\x9a\x99##cfg", ImVec2(kBtnW, kBtnH)))
|
|
ImGui::OpenPopup("##settings");
|
|
ImGui::PopStyleColor(2);
|
|
|
|
if (ImGui::BeginPopup("##settings")) {
|
|
if (ImGui::MenuItem("Center on player"))
|
|
pushCmd(UICmd::Recenter);
|
|
|
|
// Sync rotation — checkmark shows live state
|
|
if (ImGui::MenuItem("Sync rotation to player camera", nullptr,
|
|
win->imMapSyncPlayer.load()))
|
|
pushCmd(UICmd::RotateSyncPlayer);
|
|
|
|
if (ImGui::MenuItem("Pan cancels follow", nullptr,
|
|
win->imPanDisablesFollow.load()))
|
|
pushCmd(UICmd::TogglePanDisablesFollow);
|
|
|
|
if (ImGui::MenuItem("Right-click recenters", nullptr,
|
|
win->imRightClickRecenter.load()))
|
|
pushCmd(UICmd::ToggleRightClickRecenter);
|
|
|
|
ImGui::Separator();
|
|
|
|
// Helper lambdas for ME setting commands (y = index, delta = value)
|
|
auto pushMEBool = [&](int idx, bool val) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::SetMEBool);
|
|
ev.y = static_cast<float>(idx);
|
|
ev.delta = val ? 1.f : 0.f;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
auto pushMEFloat = [&](int idx, float val) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::SetMEFloat);
|
|
ev.y = static_cast<float>(idx);
|
|
ev.delta = val;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
|
|
bool meOn = win->imMapEnhancerInstalled.load();
|
|
bool meTurntable = win->imMEHasTurntable.load(); // v1.6.0 or community
|
|
bool meCom = win->imMECommunity.load(); // community only
|
|
if (meOn) {
|
|
if (ImGui::BeginMenu("Map Enhancer")) {
|
|
if (ImGui::MenuItem("Toggle follow mode"))
|
|
pushCmd(UICmd::FollowMode);
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Follow")) {
|
|
if (ImGui::MenuItem("Player camera"))
|
|
pushCmd(UICmd::FollowPlayerCam);
|
|
if (ImGui::MenuItem("Selected loco"))
|
|
pushCmd(UICmd::FollowSelectedLoco);
|
|
if (ImGui::MenuItem("Stop following"))
|
|
pushCmd(UICmd::FollowMode);
|
|
{
|
|
std::lock_guard<std::mutex> lk(win->imLocoMutex);
|
|
if (!win->imLocoList.empty()) {
|
|
ImGui::Separator();
|
|
for (int i = 0; i < (int)win->imLocoList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocoList[i].label))
|
|
pushCmd(UICmd::FollowLoco, (float)i);
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Jump to Location")) {
|
|
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
|
if (win->imLocationList.empty())
|
|
ImGui::TextDisabled("(loading...)");
|
|
else
|
|
for (int i = 0; i < (int)win->imLocationList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocationList[i].label))
|
|
pushCmd(UICmd::JumpToLocation, (float)i);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginMenu("Settings")) {
|
|
uint32_t meFlags = win->imMEFlags.load();
|
|
auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; };
|
|
|
|
// -- Markers --
|
|
// Turntable Markers: available on v1.6.0 and community (ShowTurntableMarkers field).
|
|
// Everything else in this block is community-only.
|
|
ImGui::TextDisabled("Markers");
|
|
ImGui::BeginDisabled(!meTurntable);
|
|
{
|
|
bool v = meBit(MB_TurntableMarkers);
|
|
if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
bool v = meBit(MB_TurntableControl);
|
|
if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v);
|
|
if (v) {
|
|
bool cl = meBit(MB_CheckClearance);
|
|
if (ImGui::MenuItem(" Check Clearance", nullptr, cl)) pushMEBool(MB_CheckClearance, !cl);
|
|
}
|
|
}
|
|
{
|
|
bool v = meBit(MB_CrossingMarkers);
|
|
if (ImGui::MenuItem("Road Crossing Markers", nullptr, v)) pushMEBool(MB_CrossingMarkers, !v);
|
|
}
|
|
{
|
|
bool v = meBit(MB_PassengerStops);
|
|
if (ImGui::MenuItem("Passenger Stop Tracking", nullptr, v)) pushMEBool(MB_PassengerStops, !v);
|
|
}
|
|
{
|
|
bool v = meBit(MB_IndustryAreaColors);
|
|
if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Behavior --
|
|
ImGui::TextDisabled("Behavior");
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
bool v = meBit(MB_ModdedSpawnPoints);
|
|
if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
{
|
|
bool v = meBit(MB_DoubleClick);
|
|
if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Scales -- (live-update atomic during drag; push on release)
|
|
ImGui::TextDisabled("Scales");
|
|
{
|
|
float v = win->imMEFlareScale.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Flare##me", &v, 0.1f, 1.0f, "%.2f"))
|
|
win->imMEFlareScale.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_FlareScale, win->imMEFlareScale.load());
|
|
}
|
|
{
|
|
float v = win->imMEJunctionSc.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Junction##me", &v, 0.5f, 1.0f, "%.2f"))
|
|
win->imMEJunctionSc.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_JunctionScale, win->imMEJunctionSc.load());
|
|
}
|
|
{
|
|
float v = win->imMETrackThick.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Track Width##me", &v, 0.5f, 2.0f, "%.2f"))
|
|
win->imMETrackThick.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_TrackThickness, win->imMETrackThick.load());
|
|
}
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
float v = win->imMECrossingSc.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Crossing##me", &v, 0.1f, 1.0f, "%.2f"))
|
|
win->imMECrossingSc.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load());
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Switch Reset (community build only) --
|
|
ImGui::BeginDisabled(!meCom);
|
|
ImGui::TextDisabled("Switch Reset");
|
|
if (ImGui::MenuItem("All Switches - Normal"))
|
|
pushCmd(UICmd::MEResetSwitchesNormal);
|
|
if (ImGui::MenuItem("All Switches - Thrown"))
|
|
pushCmd(UICmd::MEResetSwitchesThrown);
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
} else {
|
|
if (ImGui::BeginMenu("Jump to Location")) {
|
|
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
|
if (win->imLocationList.empty())
|
|
ImGui::TextDisabled("(loading...)");
|
|
else
|
|
for (int i = 0; i < (int)win->imLocationList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocationList[i].label))
|
|
pushCmd(UICmd::JumpToLocation, (float)i);
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Theme preset picker
|
|
if (ImGui::BeginMenu("Theme")) {
|
|
int cur = g_currentThemePreset.load();
|
|
if (ImGui::MenuItem("S3 Dark", nullptr, cur == 0)) pushCmd(UICmd::SetTheme, 0.f);
|
|
if (ImGui::MenuItem("Railroader Classic", nullptr, cur == 1)) pushCmd(UICmd::SetTheme, 1.f);
|
|
if (ImGui::MenuItem("Night Mode", nullptr, cur == 2)) pushCmd(UICmd::SetTheme, 2.f);
|
|
if (ImGui::MenuItem("High Visibility", nullptr, cur == 3)) pushCmd(UICmd::SetTheme, 3.f);
|
|
if (ImGui::MenuItem("Retro Terminal", nullptr, cur == 4)) pushCmd(UICmd::SetTheme, 4.f);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Custom", nullptr, cur == 5)) pushCmd(UICmd::SetTheme, 5.f);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
// Opacity sliders (in-game overlay only — popout is a native OS window)
|
|
if (showPopOutBtn) {
|
|
ImGui::Separator();
|
|
// Chrome alpha — window frame, toolbar, compass
|
|
float alpha = g_ovAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Window##ov", &alpha, 0.1f, 1.0f, "%.2f"))
|
|
g_ovAlpha.store(alpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetAlpha, g_ovAlpha.load());
|
|
// Map image alpha — independent of chrome
|
|
float mAlpha = g_mapAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Map Elements##ov", &mAlpha, 0.0f, 1.0f, "%.2f"))
|
|
g_mapAlpha.store(mAlpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetMapAlpha, g_mapAlpha.load());
|
|
// Map background alpha — camera clear colour opacity (0 = transparent void)
|
|
float bgAlpha = g_mapBgAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Map Background##ov", &bgAlpha, 0.0f, 1.0f, "%.2f"))
|
|
g_mapBgAlpha.store(bgAlpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetMapBgAlpha, g_mapBgAlpha.load());
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::End();
|
|
}
|
|
|
|
void Renderer_Present(PopoutWindow* win) {
|
|
if (!win || win->resizing.load()) return;
|
|
void* rawTex = win->pendingTexture.load();
|
|
if (!rawTex) return;
|
|
|
|
// Lazy device init from the first texture if UnityPluginLoad didn't fire
|
|
if (!g_device) {
|
|
auto* tex = static_cast<ID3D11Texture2D*>(rawTex);
|
|
ID3D11Device* dev = nullptr;
|
|
tex->GetDevice(&dev);
|
|
if (!dev) return;
|
|
Renderer_OnDeviceInit(dev);
|
|
dev->Release();
|
|
}
|
|
if (!g_device || !g_context) return;
|
|
if (!Renderer_EnsureSwapchain(win)) return;
|
|
|
|
auto* srcTex = static_cast<ID3D11Texture2D*>(rawTex);
|
|
D3D11_TEXTURE2D_DESC texDesc;
|
|
srcTex->GetDesc(&texDesc);
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = TypedFormat(texDesc.Format);
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MostDetailedMip = 0;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
ComPtr<ID3D11ShaderResourceView> srv;
|
|
if (FAILED(g_device->CreateShaderResourceView(srcTex, &srvDesc, &srv))) return;
|
|
|
|
// Save Unity's render state
|
|
D3D11StateBlock saved;
|
|
saved.Capture(g_context);
|
|
|
|
int w = win->width.load(), h = win->height.load();
|
|
D3D11_VIEWPORT vp { 0.f, 0.f, (float)w, (float)h, 0.f, 1.f };
|
|
|
|
// Update UV rect constant buffer
|
|
if (g_uvRectCB) {
|
|
D3D11_MAPPED_SUBRESOURCE mapped = {};
|
|
if (SUCCEEDED(g_context->Map(g_uvRectCB.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) {
|
|
auto* cb = static_cast<UVRectCB*>(mapped.pData);
|
|
cb->u0 = win->srcU0.load(); cb->v0 = win->srcV0.load();
|
|
cb->u1 = win->srcU1.load(); cb->v1 = win->srcV1.load();
|
|
g_context->Unmap(g_uvRectCB.Get(), 0);
|
|
}
|
|
}
|
|
|
|
// Blit map texture into swapchain back buffer
|
|
float bf[4] = {};
|
|
g_context->OMSetRenderTargets(1, &win->rtv, nullptr);
|
|
g_context->RSSetViewports(1, &vp);
|
|
g_context->VSSetShader(g_vs.Get(), nullptr, 0);
|
|
ID3D11Buffer* cbPtr = g_uvRectCB.Get();
|
|
g_context->VSSetConstantBuffers(0, 1, &cbPtr);
|
|
g_context->PSSetShader(g_ps.Get(), nullptr, 0);
|
|
g_context->IASetInputLayout(nullptr);
|
|
g_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
ID3D11ShaderResourceView* srvPtr = srv.Get();
|
|
g_context->PSSetShaderResources(0, 1, &srvPtr);
|
|
ID3D11SamplerState* sampPtr = g_sampler.Get();
|
|
g_context->PSSetSamplers(0, 1, &sampPtr);
|
|
g_context->OMSetBlendState(g_blendOpaque.Get(), bf, 0xFFFFFFFF);
|
|
g_context->RSSetState(g_rasterState.Get());
|
|
g_context->OMSetDepthStencilState(g_depthState.Get(), 0);
|
|
g_context->Draw(3, 0);
|
|
|
|
// -----------------------------------------------------------------------
|
|
// ImGui overlay — compass rose on map + toolbar strip at bottom
|
|
// -----------------------------------------------------------------------
|
|
if (g_imguiInited) {
|
|
MapThemeData theme = SnapshotTheme();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2((float)w, (float)h);
|
|
io.MousePos = ImVec2(win->imMouseX.load(), win->imMouseY.load());
|
|
io.MouseDown[0] = win->imLButton.load();
|
|
io.MouseDown[1] = win->imRButton.load();
|
|
int rawWheel = win->imWheelRaw.exchange(0);
|
|
io.MouseWheel = (float)rawWheel / WHEEL_DELTA;
|
|
|
|
ImGui_ImplDX11_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
BuildMapUI(win, ImVec2(0.f, 0.f), (float)w, (float)h,
|
|
/*reserveResizeGrip=*/false, /*showPopOutBtn=*/false, theme);
|
|
|
|
ImGui::Render();
|
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
|
win->imWantMouse.store(ImGui::GetIO().WantCaptureMouse);
|
|
}
|
|
|
|
win->swapChain->Present(0, 0);
|
|
|
|
// Restore Unity's render state
|
|
saved.Restore(g_context);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// In-game overlay (spike)
|
|
// ===========================================================================
|
|
// Renders an ImGui test window into Unity's currently bound backbuffer (no
|
|
// swapchain, no Present) to prove the on-top render path. Reuses the SAME ImGui
|
|
// context + DX11 backend the popout uses (set up once in InitImGui): the popout
|
|
// and overlay each do a complete NewFrame/Render inside their own plugin event
|
|
// on the render thread, so sequential reuse of one context is safe. A second
|
|
// context tripped the DX11 backend's per-context init in release builds.
|
|
// ---------------------------------------------------------------------------
|
|
static std::atomic<void*> g_ovDeviceTex {nullptr};
|
|
static std::atomic<float> g_ovW {0.f}, g_ovH {0.f};
|
|
static std::atomic<float> g_ovMouseX {-1.f}, g_ovMouseY {-1.f};
|
|
static std::atomic<bool> g_ovLButton {false}, g_ovRButton {false};
|
|
static std::atomic<int> g_ovWheelRaw {0};
|
|
static std::atomic<bool> g_ovWantMouse {false};
|
|
static std::atomic<bool> g_ovVisible {false};
|
|
|
|
// Map texture to show in the in-game window (set from C# each frame) + UV rect.
|
|
static std::atomic<void*> g_ovMapTex {nullptr};
|
|
static std::atomic<float> g_ovMapU0 {0.f}, g_ovMapV0 {1.f};
|
|
static std::atomic<float> g_ovMapU1 {1.f}, g_ovMapV1 {0.f};
|
|
// SRV over the map texture for ImGui::Image. Rebuilt each frame; must outlive
|
|
// RenderDrawData, so it lives at file scope rather than as a frame-local.
|
|
static ComPtr<ID3D11ShaderResourceView> g_ovMapSRV;
|
|
|
|
// Size (px) of the map image region this frame. C# reads it back to set the map
|
|
// camera's aspect to the window shape, so the map fills the window (no letterbox).
|
|
static std::atomic<float> g_ovViewW {0.f}, g_ovViewH {0.f};
|
|
void Overlay_GetMapView(float* outW, float* outH) {
|
|
if (outW) *outW = g_ovViewW.load();
|
|
if (outH) *outH = g_ovViewH.load();
|
|
}
|
|
|
|
void Overlay_SetAlpha(float alpha) { g_ovAlpha.store(std::max(0.1f, std::min(1.0f, alpha))); }
|
|
void Overlay_SetMapAlpha(float alpha) { g_mapAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
|
void Overlay_SetMapBgAlpha(float alpha) { g_mapBgAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
|
|
|
// Map-image interaction (drag/zoom) queued here for C# to forward to the map
|
|
// camera. Same InputEvent contract the popout uses, so C# can share the logic.
|
|
static InputQueue g_ovInput;
|
|
int Overlay_PollInput(InputEvent* out, int maxEvents) { return g_ovInput.drain(out, maxEvents); }
|
|
|
|
void Overlay_SetDeviceTexture(void* texturePtr) { g_ovDeviceTex.store(texturePtr); }
|
|
void Overlay_SetVisible(bool visible) { g_ovVisible.store(visible); }
|
|
bool Overlay_WantsMouse() { return g_ovWantMouse.load(); }
|
|
|
|
void Overlay_SetMapTexture(void* texturePtr, float u0, float v0, float u1, float v1) {
|
|
g_ovMapTex.store(texturePtr);
|
|
g_ovMapU0.store(u0); g_ovMapV0.store(v0);
|
|
g_ovMapU1.store(u1); g_ovMapV1.store(v1);
|
|
}
|
|
|
|
void Overlay_SetInput(float displayW, float displayH,
|
|
float mouseX, float mouseY,
|
|
bool lButton, bool rButton, int wheel) {
|
|
g_ovW.store(displayW); g_ovH.store(displayH);
|
|
g_ovMouseX.store(mouseX); g_ovMouseY.store(mouseY);
|
|
g_ovLButton.store(lButton); g_ovRButton.store(rButton);
|
|
if (wheel) g_ovWheelRaw.fetch_add(wheel);
|
|
}
|
|
|
|
void Renderer_PresentOverlay() {
|
|
if (!g_ovVisible.load()) return;
|
|
|
|
// Lazy device init — same path the popout uses, but from a throwaway texture
|
|
// since the overlay has no map RT of its own. Renderer_OnDeviceInit also runs
|
|
// InitImGui (shared context + DX11 backend), so g_imguiInited is set here too.
|
|
if (!g_device) {
|
|
void* t = g_ovDeviceTex.load();
|
|
if (!t) return;
|
|
auto* tex = static_cast<ID3D11Texture2D*>(t);
|
|
ID3D11Device* dev = nullptr;
|
|
tex->GetDevice(&dev);
|
|
if (!dev) return;
|
|
Renderer_OnDeviceInit(dev);
|
|
dev->Release();
|
|
}
|
|
if (!g_device || !g_context || !g_imguiInited) return;
|
|
|
|
float w = g_ovW.load(), h = g_ovH.load();
|
|
if (w <= 0.f || h <= 0.f) return;
|
|
|
|
// Save Unity's render state — we draw into its currently bound RTV and leave
|
|
// it bound (no OMSetRenderTargets), so ImGui composites onto the backbuffer.
|
|
D3D11StateBlock saved;
|
|
saved.Capture(g_context);
|
|
|
|
MapThemeData theme = SnapshotTheme();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2(w, h);
|
|
io.MousePos = ImVec2(g_ovMouseX.load(), g_ovMouseY.load());
|
|
io.MouseDown[0] = g_ovLButton.load();
|
|
io.MouseDown[1] = g_ovRButton.load();
|
|
io.MouseWheel = (float)g_ovWheelRaw.exchange(0) / WHEEL_DELTA;
|
|
|
|
ImGui_ImplDX11_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
// Chrome (window bg, title bar, toolbar, compass) alpha. Pushed before Begin()
|
|
// so all decorations are affected; popped around the map Image() so the map
|
|
// image has its own independent alpha, then popped finally after BuildMapUI.
|
|
const float ovAlpha = g_ovAlpha.load();
|
|
const float mapAlpha = g_mapAlpha.load();
|
|
const float mapBgA = g_mapBgAlpha.load();
|
|
// While the mouse is over the overlay, clamp chrome alpha to 50% so the UI
|
|
// remains readable even when the user has set a very low window opacity.
|
|
// Uses last frame's WantCaptureMouse (one-frame lag; imperceptible in practice).
|
|
const bool mouseOver = g_ovWantMouse.load();
|
|
const float effectiveAlpha = mouseOver ? std::max(ovAlpha, 0.5f) : ovAlpha;
|
|
const bool hasAlpha = effectiveAlpha < 0.999f;
|
|
if (hasAlpha)
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
|
|
|
g_ovMapSRV.Reset(); // release last frame's view before building this frame's
|
|
|
|
// Captured inside the window, used to position the shared chrome (toolbar +
|
|
// compass) over the map image after the window's End().
|
|
ImVec2 mapScreenPos(0.f, 0.f), mapSize(0.f, 0.f);
|
|
bool mapShown = false;
|
|
|
|
// Shared UI-state holder (status, lists, rotation) + command queue for chrome.
|
|
PopoutWindow* stateWin = GetPopoutWindow(GetOrCreateOverlayStateHandle());
|
|
|
|
// Zero padding so the map fills the window edge-to-edge and the toolbar sits
|
|
// flush at the bottom (acting as the border) with no dark frame around it.
|
|
// WindowPadding is only read during Begin() so we pop it right after — this
|
|
// keeps it off the style stack so we can freely pop/push ovAlpha around Image().
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
|
ImGui::SetNextWindowPos(ImVec2(80.f, 80.f), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(720.f, 480.f), ImGuiCond_FirstUseEver);
|
|
// Scale window bg alpha by mapBgA so the map background slider makes the void
|
|
// behind the camera transparent (showing Unity's scene), not the window chrome.
|
|
if (mapBgA < 0.999f)
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
|
ImVec4(theme.wBgR, theme.wBgG, theme.wBgB, theme.wBgA * mapBgA));
|
|
// p_open = true: ImGui adds an [X] close button to the title bar. When the user
|
|
// clicks it ImGui sets windowOpen=false; we push UICmd::Close so C# can call
|
|
// HideIfVisible() and DeactivateMap(). NoBringToFrontOnFocus keeps this window
|
|
// behind the chrome windows (toolbar + compass) that BuildMapUI creates after it.
|
|
bool windowOpen = true;
|
|
bool windowVisible = ImGui::Begin("Railroader Map##s3ingame", &windowOpen,
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
|
// WindowPadding is read; pop now so ovAlpha is the only active style var.
|
|
ImGui::PopStyleVar();
|
|
if (mapBgA < 0.999f) ImGui::PopStyleColor();
|
|
|
|
if (windowVisible) {
|
|
void* mapTex = g_ovMapTex.load();
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
|
|
if (mapTex && avail.x > 4.f && avail.y > 4.f) {
|
|
auto* tex = static_cast<ID3D11Texture2D*>(mapTex);
|
|
D3D11_TEXTURE2D_DESC td; tex->GetDesc(&td);
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC sd = {};
|
|
sd.Format = TypedFormat(td.Format);
|
|
sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
sd.Texture2D.MostDetailedMip = 0;
|
|
sd.Texture2D.MipLevels = 1;
|
|
if (SUCCEEDED(g_device->CreateShaderResourceView(tex, &sd, &g_ovMapSRV))) {
|
|
// Fill the whole content region (no letterbox). C# reads this size
|
|
// back and sets camera.aspect to match, so the map renders the right
|
|
// slice to fill the window instead of leaving bars top/bottom.
|
|
ImVec2 sz = avail;
|
|
g_ovViewW.store(sz.x);
|
|
g_ovViewH.store(sz.y);
|
|
|
|
// Transparent hit area over the map for drag/zoom. Queue events in
|
|
// normalized [0,1] image space (top-left origin) so C# can forward
|
|
// them to the map camera with the same math the popout uses.
|
|
ImVec2 imgPos = ImGui::GetCursorScreenPos();
|
|
ImGui::InvisibleButton("##mapHit", sz, ImGuiButtonFlags_MouseButtonLeft);
|
|
bool hov = ImGui::IsItemHovered();
|
|
ImVec2 nrm = { (io.MousePos.x - imgPos.x) / sz.x,
|
|
(io.MousePos.y - imgPos.y) / sz.y };
|
|
auto pushOv = [&](int type, float d) {
|
|
InputEvent e{}; e.type = type; e.x = nrm.x; e.y = nrm.y; e.delta = d;
|
|
g_ovInput.push(e);
|
|
};
|
|
if (ImGui::IsItemActivated()) pushOv(LButtonDown, 0.f);
|
|
if (ImGui::IsItemActive() &&
|
|
(io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f))
|
|
pushOv(MouseMove, 0.f);
|
|
if (ImGui::IsItemDeactivated()) pushOv(LButtonUp, 0.f);
|
|
if (hov && io.MouseWheel != 0.f) pushOv(MouseWheel, io.MouseWheel);
|
|
if (hov && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) pushOv(RButtonUp, 0.f);
|
|
|
|
// Draw the map over the same rect the hit-button occupies.
|
|
// Map alpha is independent of chrome alpha: pop ovAlpha so
|
|
// ImGuiStyleVar_Alpha doesn't multiply into the tint, then push back.
|
|
ImGui::SetCursorScreenPos(imgPos);
|
|
ImVec2 uv0(g_ovMapU0.load(), g_ovMapV0.load());
|
|
ImVec2 uv1(g_ovMapU1.load(), g_ovMapV1.load());
|
|
ImVec4 tint(theme.mapR, theme.mapG, theme.mapB, theme.mapA * mapAlpha);
|
|
if (hasAlpha) ImGui::PopStyleVar(); // lift chrome alpha
|
|
ImGui::Image((ImTextureID)g_ovMapSRV.Get(), sz, uv0, uv1, tint);
|
|
if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
|
|
|
// Visible resize-grip indicator. ImGui draws its own grip during
|
|
// Begin() — behind our opaque map image, so invisible. We draw one
|
|
// on top in the corner only while the window is focused (the dark
|
|
// unfocused version was more distracting than no grip at all).
|
|
if (ImGui::IsWindowFocused()) {
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 br(imgPos.x + sz.x, imgPos.y + sz.y);
|
|
const float gripSz = 16.f;
|
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TitleBgActive, 1.0f);
|
|
dl->AddTriangleFilled(ImVec2(br.x, br.y),
|
|
ImVec2(br.x - gripSz, br.y),
|
|
ImVec2(br.x, br.y - gripSz), col);
|
|
}
|
|
|
|
mapScreenPos = imgPos; mapSize = sz; mapShown = true;
|
|
}
|
|
} else {
|
|
ImGui::TextWrapped("Load a save and open the in-game map, then it will "
|
|
"appear here. (F10 toggles this window.)");
|
|
}
|
|
}
|
|
ImGui::End();
|
|
// WindowPadding was already popped right after Begin(); nothing extra to pop here.
|
|
|
|
// If the user clicked [X], push a Close command for C# to process.
|
|
if (!windowOpen && stateWin) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::Close);
|
|
stateWin->inputQueue.push(ev);
|
|
}
|
|
|
|
// Shared map chrome (toolbar + compass), positioned over the map image so the
|
|
// in-game map and the popout present the same UI. Reads the shared state holder.
|
|
if (mapShown && stateWin)
|
|
BuildMapUI(stateWin, mapScreenPos, mapSize.x, mapSize.y,
|
|
/*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme, mapAlpha);
|
|
|
|
// Pop the global alpha AFTER all chrome windows, before Render().
|
|
if (hasAlpha)
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::Render();
|
|
|
|
// Bind a linear (non-sRGB) view of Unity's backbuffer so the GPU does not
|
|
// sRGB-encode our writes (which washes the map/UI out). This makes the overlay
|
|
// match the popout, which renders into its own UNORM swapchain. Falls back to
|
|
// Unity's bound RTV if a UNORM view can't be created (no regression).
|
|
ComPtr<ID3D11RenderTargetView> linearRtv;
|
|
if (saved.rtv[0]) {
|
|
ComPtr<ID3D11Resource> res;
|
|
saved.rtv[0]->GetResource(&res);
|
|
ComPtr<ID3D11Texture2D> bb;
|
|
if (res && SUCCEEDED(res.As(&bb))) {
|
|
D3D11_TEXTURE2D_DESC bbDesc; bb->GetDesc(&bbDesc);
|
|
DXGI_FORMAT linear = UnormRtvFormat(bbDesc.Format);
|
|
if (linear != DXGI_FORMAT_UNKNOWN) {
|
|
D3D11_RENDER_TARGET_VIEW_DESC rd = {};
|
|
rd.Format = linear;
|
|
rd.ViewDimension = (bbDesc.SampleDesc.Count > 1)
|
|
? D3D11_RTV_DIMENSION_TEXTURE2DMS : D3D11_RTV_DIMENSION_TEXTURE2D;
|
|
g_device->CreateRenderTargetView(bb.Get(), &rd, &linearRtv);
|
|
}
|
|
}
|
|
}
|
|
if (linearRtv) {
|
|
ID3D11RenderTargetView* rtv = linearRtv.Get();
|
|
g_context->OMSetRenderTargets(1, &rtv, nullptr);
|
|
}
|
|
|
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
|
g_ovWantMouse.store(io.WantCaptureMouse);
|
|
|
|
// Restore Unity's render state
|
|
saved.Restore(g_context);
|
|
}
|