Render services and samples for SimCity 4 DLL plugins built on the gzcom-dll SDK: ImGui UI, drawing into the city, the S3D camera, draw pass hooks and terrain decals.
Everything is drawn through the game's own graphics driver interface (cIGZGDriver), so
plugins work the same whichever driver the game uses: its built-in DirectX driver, SCD3D11 or
scvk.
Simtropolis file download page: https://community.simtropolis.com/files/file/37423-sc4renderservices-dll/
| Feature | Built-in DirectX | SCD3D11 | scvk |
|---|---|---|---|
| ImGui panels, docking, textures | yes | yes | yes |
World drawing (cIGZWorldDrawService) |
yes | yes | yes |
| Draw pass callbacks and draw context helpers | yes | yes | yes |
| S3D camera service | yes | yes | yes |
| Terrain decal service | yes | yes | yes |
| Terrain decal shadow recovery | yes | yes (stencil) | yes (stencil from 0.4.0) |
Raw DirectX 7 access (AcquireD3DInterfaces) |
yes | no | no |
cIGZImGuiService::GetActiveDriverInfo tells a plugin which driver is active.
- Copy
SC4RenderServices.dllandSC4RenderServices.iniinto your Plugins folder (e.g.Documents\SimCity 4\Plugins\). - (Optional) Copy any sample DLLs you want into the same Plugins folder.
- Edit
SC4RenderServices.inito customise logging, appearance, and which services are loaded. See Configuration below.
Older versions also installed imgui.dll in the game's Apps folder. ImGui is now built into
SC4RenderServices.dll, so that file is no longer used. Delete it once every plugin you use
that depends on SC4RenderServices has been updated: plugins built for the old version still
need it to load, and they get no ImGui service from this version either way.
The services require SimCity 4 version 1.1.641.
SC4RenderServices.ini must sit next to SC4RenderServices.dll in the Plugins folder. All
settings are optional. If the file is missing or a key is absent, the default value is used.
[SC4RenderServices]
; Logging verbosity.
; Valid values: trace, debug, info, warn, error, critical, off
LogLevel=info
; Write logs to file. Set to false for MSVC debug output only.
LogToFile=true
; ImGui base font size in pixels. Valid range: 8.0 - 32.0
FontSize=13.0
; Custom .ttf font file path (relative to DLL folder).
; Leave empty to use the built-in ProggyVector font.
FontFile=
; Font oversampling level (1-3). Higher = crisper text, more memory.
FontOversample=2
; ImGui color theme. Valid values: dark, light, classic, forestgreen, amethyst, sapphire, amberyellow,
; dracula, catppuccinmocha, gruvboxhard, crimsonvesuvius, rosequartz, cyberpunk, paperandink
; Pictures of these themes: https://github.com/ocornut/imgui/issues/707#issuecomment-4107169777
; In the game, frames and borders are a bit brighter than in the pictures.
Theme=dark
; Enable ImGui keyboard navigation.
KeyboardNav=true
; Global UI scale factor. Valid range: 0.5 - 3.0
UIScale=1.0
; File that stores ImGui window positions and the docking layout (relative to DLL folder).
; Leave empty to not save the layout between sessions.
LayoutFile=SC4RenderServices_layout.ini
; Show a built-in status panel and the Dear ImGui demo window on startup.
; Useful for verifying that the service installed correctly.
ShowDemoPanel=false
; Enable or disable individual services.
; The world draw service is part of the draw service and follows EnableDrawService.
EnableImGuiService=true
EnableS3DCameraService=true
EnableDrawService=true
EnableTerrainDecalService=true
; Enables the custom terrain decal renderer path used to improve vanilla
; terrain decal rendering with UV subrect support, clipping, and depth offsets.
EnableCustomTerrainDecalRenderer=true
; Default depth offset applied to custom-rendered terrain decals.
; Vanilla terrain decals use 2. Shadows use 3. Valid range: 0 - 64.
TerrainDecalCustomDefaultDepthOffset=2
; Opacity scale for the post-shadow terrain decal recovery redraw.
; Lower values blend more softly with shadows. 0 turns the redraw off. Valid range: 0.0 - 1.0.
TerrainDecalShadowRecoveryOpacityScale=0.25
; Draws the shadow recovery redraw once per pixel, from the topmost decal, using the
; stencil buffer, so overlapping decals do not come out darker. Used with graphics drivers
; that have a stencil buffer, such as SCD3D11 and scvk 0.4.0 or later. The game's built-in
; DirectX driver keeps its own method. Set to false to draw every decal again instead.
TerrainDecalShadowRecoveryUseStencil=truePrereqs:
- CMake 4.0+
- Visual Studio 2022 (Win32 toolchain, SimCity 4 only loads 32-bit DLLs)
- Submodules:
git submodule update --init --recursive
With the presets:
cmake --preset vs2022-win32-release
cmake --build --preset vs2022-win32-release-all
Or by hand:
cmake -S . -B cmake-build-debug-visual-studio -G "Visual Studio 17 2022" -A Win32 -DCMAKE_BUILD_TYPE=Debug
cmake --build cmake-build-debug-visual-studio --config Debug
Pass -DCMAKE_BUILD_TYPE=Debug for a Debug build: it picks the matching debug C runtime.
Only SC4RenderServices.dll is built by default. Add -DSC4RS_BUILD_SAMPLES=ON to build the
sample DLLs too.
When the build finds SimCity 4 it copies the DLLs into your Plugins folder. Set the CI
environment variable to turn that off.
A plugin can add this repository with add_subdirectory. Pass EXCLUDE_FROM_ALL so only what
the plugin links gets built, and link the exported ImGui with sc4rs_use_imgui:
add_subdirectory(vendor/sc4-render-services EXCLUDE_FROM_ALL)
sc4rs_use_imgui(MyPlugin)As a subproject it leaves the generator platform to the parent, never builds the samples, and
does not deploy to the Plugins folder. It reuses the parent's spdlog or spdlog::spdlog target
and its gzcom2 or gzcom_dll target when those exist, so that gzcom-dll must be at least as new
as the one in vendor/gzcom-dll.
Required (deployed to ...\Documents\SimCity 4\Plugins\):
SC4RenderServices.dllSC4RenderServices.ini
Optional samples (built with -DSC4RS_BUILD_SAMPLES=ON, 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)SC4WorldDrawSample.dll(every world drawing feature)SC4RoadDecalSample.dll(road marking tool on world draw layers)SC4WorldProjectionSample.dll(world projection helpers, depth readback on the built-in DirectX driver only)SC4S3DCameraDebugSample.dll(camera debug tooling)SC4OverlayManagerSample.dll(overlay manager example)SC4DrawServiceSample.dll(draw service UI + hooks)SC4TerrainDecalSample.dll(terrain decal editor and API sample)SC4CameraViewInputSample.dll(camera view input control)SC4DateJumperSample.dll(date jumper panel)
SC4RenderServices.dll registers five services, all gated on SimCity 4 version 1.1.641.
Detailed reference: docs/services.md
Guides:
- World drawing: docs/world-drawing.md
- Terrain decals: docs/terrain-decals.md
- Updating a plugin from the
imgui.dllversion: docs/migrating.md
- Service ID / IID:
kImGuiServiceID,GZIID_cIGZImGuiService(src/public/ImGuiServiceIds.h) - Interface:
cIGZImGuiService(src/public/cIGZImGuiService.h) - Provides panel registration, per-frame callbacks, font registration, managed textures for
ImGui::Image(), and which graphics driver is active. ImGui is the docking branch. - Examples:
SC4ImGuiSample.dll,SC4ImGuiSampleDemo.dll,SC4ImGuiTextureSample.dll,SC4ImGuiSampleCity.dll
- Service ID / IID:
kWorldDrawServiceID,GZIID_cIGZWorldDrawService(src/public/cIGZWorldDrawService.h) - Interface:
cIGZWorldDrawService, plus the header-onlySC4WorldMeshBuilder(src/public/SC4WorldMeshBuilder.h) - Draws shapes, lines and textures into the 3D city: layers that SC4RenderServices draws every frame, and per-frame draw callbacks. Also picks terrain under the mouse.
- Examples:
SC4WorldDrawSample.dll,SC4RoadDecalSample.dll
- Service ID / IID:
kS3DCameraServiceID,GZIID_cIGZS3DCameraService(src/public/S3DCameraServiceIds.h) - Interface:
cIGZS3DCameraService(src/public/cIGZS3DCameraService.h) - Provides camera creation/wrapping, world/screen projection helpers, viewport and transform operations.
- Examples:
SC4WorldProjectionSample.dll,SC4S3DCameraDebugSample.dll
- Service ID / IID:
kDrawServiceID,GZIID_cIGZDrawService(src/public/cIGZDrawService.h) - Interface:
cIGZDrawService(src/public/cIGZDrawService.h) - Provides draw-context wrapping, render-pass callbacks
(
PreStatic...PostDynamic), and helpers for render state, mesh/model draw calls, and primitive drawing. For drawing your own shapes, the world draw service is simpler and handles the render state for you. - Example:
SC4DrawServiceSample.dll
- Service ID / IID:
kTerrainDecalServiceID,GZIID_cIGZTerrainDecalService(src/public/TerrainDecalServiceIds.h) - Interface:
cIGZTerrainDecalService(src/public/cIGZTerrainDecalService.h) - Provides managed create/update/remove/enumerate APIs for terrain decals, save/load persistence for managed decals, and optional UV sub-rectangle overrides.
- Example:
SC4TerrainDecalSample.dll
-
Query a service via
cIGZFrameWork::GetSystemServicewith its service ID and IID. The returned interface is AddRef'd, soRelease()it when done. -
To use ImGui, include
vendor/imgui/imgui.hand link the ImGui thatSC4RenderServices.dllexports. In this repository,sc4rs_use_imgui(target)inCMakeLists.txtdoes everything. A plugin in its own repository needs the same four things:- define
IMGUI_USER_CONFIG="ImGuiUserConfig.h"andIMGUI_DLL_IMPORT, withsrc/publicon the include path - link
SC4RenderServices.libanddelayimp.lib - link with
/DELAYLOAD:SC4RenderServices.dll - compile
src/public/SC4RenderServicesDelayLoad.cppinto the plugin
Delay loading makes the plugin independent of the order the game loads DLLs in. Only call ImGui once you hold the ImGui service.
- define
-
Register a panel with
ImGuiPanelDescand render usingImGui::*. -
The service owns the ImGui context. Do not call
ImGui::CreateContext()orImGui::DestroyContext()in clients.
Four callback models are available:
cIGZImGuiServicepanel callbacks (on_init,on_update,on_render,on_visible_changed,on_shutdown,on_unregister,on_device_lost,on_device_restored) for panel lifecycle and per-frame UI drawing.cIGZImGuiService::QueueRender(...)for one-shot work on the next ImGui frame. The optional cleanup callback runs after execution (or during shutdown if still queued).cIGZWorldDrawService::RegisterDrawCallback(...)to draw into the city at the end of a pass every frame. See docs/world-drawing.md.cIGZDrawService::RegisterDrawPassCallback(...)for raw renderer pass hooks. Callback signature isvoid(DrawServicePass pass, bool begin, void* userData). The callback is invoked twice per pass: before the game pass (begin=true) and after it (begin=false).
Minimal draw-pass callback pattern:
static void OnDrawPass(DrawServicePass pass, bool begin, void* userData) {
if (pass == DrawServicePass::Dynamic && begin) {
// Inject custom draw setup/work before Dynamic pass.
}
}
uint32_t token = 0;
if (drawService->RegisterDrawPassCallback(DrawServicePass::Dynamic,
&OnDrawPass, myData, &token)) {
// Keep token and call UnregisterDrawPassCallback(token) during shutdown.
}Client-facing headers live under src/public:
cIGZImGuiService.h(service interface +ImGuiPanelDesc) andImGuiServiceIds.hImGuiPanel.handImGuiPanelAdapter.h(optional class-style adapter)ImGuiTexture.h(RAII texture wrapper)GraphicsDriverInfo.h(which graphics driver is active)cIGZWorldDrawService.handSC4WorldMeshBuilder.h(world drawing)cIGZDrawService.h,cIGZS3DCameraService.h,cIGZTerrainDecalService.hand their ID headers
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 creates textures through the active graphics driver and keeps a copy of the pixels,
so it can recreate them when the driver loses them. This is the recommended way to create
textures for ImGui::Image() and for textured world drawing.
- Device loss handling: textures are recreated after Alt+Tab, a resolution change or a new driver object
- Generation tracking: old texture handles are invalidated after a device reset
- Memory safety: source pixel data is stored for automatic recreation
- 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"
ImGuiTextureDesc desc{};
desc.width = 128;
desc.height = 128;
desc.pixels = myRGBA32Data; // Must remain valid during call
// 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);GetTextureID returns the driver's texture name cast to void*. Treat it as an opaque
ImTextureID. ImGuiTextureDesc::useSystemMemory is ignored: the driver decides where the
texture lives.
When the graphics device is reset, the device generation increments and 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 - RGBA32 format: Pixel data must be in RGBA32 format (4 bytes per pixel: R, G, B, A)
- Thread safety: Not thread-safe - call from the game's main thread only
- Lifetime: Release textures before unregistering panels or shutting down
- Generation mismatch: Handles from old device generations return
nullptrfromGetTextureID()
ImGuiTextureHandle handle = service->CreateTexture(desc);
if (handle.id == 0) {
LOG_ERROR("Texture creation failed");
return;
}
void* texId = service->GetTextureID(handle);
if (!texId) {
// Device lost, generation mismatch, or recreation failed.
if (!service->IsTextureValid(handle)) {
// Handle is stale (generation mismatch): create a new texture
}
}See src/sample/ImGuiTextureSample.cpp for a complete working example.
SC4GraphicsDriverInfo driver{};
if (service->GetActiveDriverInfo(driver)) {
// driver.kind: BuiltInDirectX, BuiltInOpenGL, SCD3D11, Scvk or Unknown
// driver.name: a display name, driver.modulePath: the DLL that implements it
}The game's driver class ID cannot tell drivers apart (SCD3D11 reports the built-in DirectX ID, scvk the OpenGL one), so the service identifies the driver by the module that implements it.
cIGZImGuiService::AcquireD3DInterfaces hands out the built-in DirectX driver's
IDirect3DDevice7 and IDirectDraw7. It returns false with any other driver, so treat it as
an optional extra and prefer the world draw service and the texture API, which work everywhere.
The service AddRef()s both interfaces and callers must Release() them. Acquire them per operation rather than caching them across frames.
IDirect3DDevice7* d3d = nullptr;
IDirectDraw7* dd = nullptr;
if (service->AcquireD3DInterfaces(&d3d, &dd)) {
// use d3d/dd
d3d->Release();
dd->Release();
}- The services are
cIGZSystemServices registered onOnStart. ImGui draws once per frame from a hook on the game's per-framecIGZGDriver::Flushcall incGZGraphicSystem::Draw, so it does not appear in region thumbnails or photos. 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. - Reverse engineering notes for the driver backend are in docs/generic-backend-re.md and docs/world-draw-design.md.
