ImGui service and samples for SimCity 4 DLL plugins using the gzcom-dll SDK.
Prereqs:
- CMake 4.0+
- Visual Studio (Win32 toolchain)
Steps (from repo root):
cmake -S . -B cmake-build-debug-visual-studio -G "Visual Studio 17 2022"
cmake --build cmake-build-debug-visual-studio --config Debug
Required:
imgui.dll(deployed to...\SimCity 4 Deluxe Edition\Apps)SC4ImGuiService.dll(deployed to...\Documents\SimCity 4\Plugins\)
Optional samples (deployed to ...\Documents\SimCity 4\Plugins\):
SC4ImGuiSample.dll(basic panel)SC4ImGuiSampleCity.dll(city-view only)SC4ImGuiSampleDemo.dll(ImGui demo window)SC4ImGuiTextureSample.dll(texture management example)
- Link your DLL against
imgui.dlland includevendor/d3d7imgui/ImGui/imgui.h. - Query the service via
cIGZFrameWork::GetSystemServicewithkImGuiServiceIDandGZIID_cIGZImGuiServicefromsrc/public/ImGuiServiceIds.h. - Register a panel with
ImGuiPanelDescand render usingImGui::*. - The service owns the ImGui context; do not call
ImGui::CreateContext()orImGui::DestroyContext()in clients.
Client-facing headers live under src/public:
src/public/cIGZImGuiService.h(service interface +ImGuiPanelDesc)src/public/ImGuiServiceIds.h(service IDs and API version)src/public/ImGuiPanel.handsrc/public/ImGuiPanelAdapter.h(optional class-style adapter)
Class-based usage (optional):
class MyPanel final : public ImGuiPanel {
public:
void OnInit() override {}
void OnRender() override {
ImGui::Begin("Class Panel");
ImGui::TextUnformatted("Hello from a class-based panel.");
ImGui::End();
}
};
void RegisterPanel(cIGZFrameWork* fw) {
cIGZImGuiService* service = nullptr;
if (!fw->GetSystemService(kImGuiServiceID, GZIID_cIGZImGuiService,
reinterpret_cast<void**>(&service))) {
return;
}
auto* panel = new MyPanel();
ImGuiPanelDesc desc = ImGuiPanelAdapter<MyPanel>::MakeDesc(panel, 0x12345678, 100, true);
if (!service->RegisterPanel(desc)) {
delete panel;
}
service->Release();
}Example (minimal):
static void RenderPanel(void* data) {
ImGui::Begin("My Panel");
ImGui::TextUnformatted("Hello from a client DLL.");
ImGui::End();
}
void RegisterPanel(cIGZFrameWork* fw) {
cIGZImGuiService* service = nullptr;
if (!fw->GetSystemService(kImGuiServiceID, GZIID_cIGZImGuiService,
reinterpret_cast<void**>(&service))) {
return;
}
ImGuiPanelDesc desc{};
desc.id = 0x12345678;
desc.order = 100;
desc.visible = true;
desc.on_render = &RenderPanel;
desc.on_shutdown = nullptr;
desc.data = nullptr;
service->RegisterPanel(desc);
service->Release();
}The service provides a comprehensive texture management API that handles device loss, memory management, and automatic recreation. This is the recommended way to create textures for use with ImGui::Image().
- Automatic device loss handling: Textures survive Alt+Tab and resolution changes
- Generation tracking: Old texture handles are automatically invalidated after device reset
- Memory safety: Source pixel data is stored for automatic recreation
- Fallback support: Automatically falls back to system memory if video memory is exhausted
- RAII wrapper: Optional
ImGuiTextureclass for automatic lifetime management
#include "public/ImGuiTexture.h"
class MyPanel {
cIGZImGuiService* service_;
ImGuiTexture myTexture_;
void OnInit() {
// Generate RGBA32 pixel data (4 bytes per pixel)
std::vector<uint8_t> pixels(128 * 128 * 4);
// ... fill pixel data ...
// Create texture - automatically manages lifetime
if (!myTexture_.Create(service_, 128, 128, pixels.data())) {
// Handle error
}
}
void OnRender() {
// Get texture ID - returns nullptr if device was lost
void* texId = myTexture_.GetID();
if (texId) {
ImGui::Image(texId, ImVec2(128, 128));
} else {
ImGui::TextUnformatted("Texture unavailable");
}
}
// Texture is automatically released in destructor
};#include "public/cIGZImGuiService.h"
// Create texture descriptor
ImGuiTextureDesc desc{};
desc.width = 128;
desc.height = 128;
desc.pixels = myRGBA32Data; // Must remain valid during call
desc.useSystemMemory = false; // Prefer video memory
// Create texture - service stores pixel data internally
ImGuiTextureHandle handle = service->CreateTexture(desc);
if (handle.id == 0) {
// Handle error
}
// In render loop:
void* texId = service->GetTextureID(handle);
if (texId) {
ImGui::Image(texId, ImVec2(128, 128));
}
// When done:
service->ReleaseTexture(handle);When the DirectX device is reset (Alt+Tab, resolution change), the device generation increments. Old texture handles become invalid:
void OnRender() {
static uint32_t lastGen = 0;
uint32_t currentGen = service->GetDeviceGeneration();
if (currentGen != lastGen) {
// Device was reset - recreate textures
RecreateMyTextures();
lastGen = currentGen;
}
// Use textures normally...
}The ImGuiTexture RAII wrapper handles this automatically by returning nullptr from GetID() when generation changes.
- Always check return values:
GetTextureID()can returnnullptrif device is lost - Store source data: The service keeps a copy, but you may want to keep your own for modification
- RGBA32 format: Pixel data must be in RGBA32 format (4 bytes per pixel: R, G, B, A)
- Thread safety: Not thread-safe - call from render thread only
- Lifetime: Release textures before unregistering panels or shutting down
- Generation mismatch: Handles from old device generations return
nullptrfromGetTextureID()
- Video memory preferred: Set
useSystemMemory = falsefor better performance (default) - Automatic fallback: Service falls back to system memory if video memory is exhausted
- On-demand recreation: Surfaces are recreated lazily when first accessed after device loss
- Minimal overhead: Device loss detection uses existing cooperative level checks
The API is designed to fail gracefully:
// Creation can fail - check handle
ImGuiTextureHandle handle = service->CreateTexture(desc);
if (handle.id == 0) {
LOG_ERROR("Texture creation failed");
return;
}
// GetTextureID returns nullptr on failure
void* texId = service->GetTextureID(handle);
if (!texId) {
// Device lost, generation mismatch, or surface recreation failed
// Check IsTextureValid() to distinguish:
if (!service->IsTextureValid(handle)) {
// Handle is stale (generation mismatch)
// Need to create new texture
}
}See src/sample/ImGuiTextureSample.cpp for a complete working example.
Note: For texture creation, prefer using the Safe Texture Management API instead of manually managing DirectDraw surfaces. The texture API handles device loss automatically.
If you need DirectX 7 interfaces for advanced use cases, use cIGZImGuiService::AcquireD3DInterfaces. The service AddRef()s both
interfaces; callers must Release() them when done. Prefer acquiring per
operation/frame rather than caching across frames. Use
IsDeviceReady() and GetDeviceGeneration() to detect when cached resources
must be recreated.
Example:
IDirect3DDevice7* d3d = nullptr;
IDirectDraw7* dd = nullptr;
if (service->AcquireD3DInterfaces(&d3d, &dd)) {
// use d3d/dd
d3d->Release();
dd->Release();
}- The service is a
cIGZSystemServiceregistered onOnStart, and it is added to the tick list viacIGZFrameWork::AddToTick. Rendering is driven from the DX7 EndScene hook once the D3D interface is available. For more info on Services, see their gzcom-dll Wiki entry. - Panels are registered with a unique
idand a simpleon_render(void* data)callback. Use thedatapointer for per-panel state instead of globals. - The ImGui context is owned by the service. Clients should not call
ImGui::CreateContext()orImGui::DestroyContext(). If you need to check readiness, callcIGZImGuiService::GetContext(). - Use the
visibleflag inImGuiPanelDescto control initial visibility.
imgui.dllmust live in the SC4Appsfolder to be loaded as a dependency.- The service owns the ImGui context and backend initialization. Clients do not
need to call
ImGui::CreateContext()orImGui::DestroyContext()or worry about hooking into the DirectX7 etc.
