Skip to content

chore(ww3d2): add IRenderBackend Interface#2613

Open
bobtista wants to merge 12 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feat/render-backend-interface-skeleton
Open

chore(ww3d2): add IRenderBackend Interface#2613
bobtista wants to merge 12 commits into
TheSuperHackers:mainfrom
bobtista:bobtista/feat/render-backend-interface-skeleton

Conversation

@bobtista

@bobtista bobtista commented Apr 17, 2026

Copy link
Copy Markdown

Summary

First PR in a planned multi-step refactor introducing an IRenderBackend interface in WW3D2. This PR adds only the scaffolding — interface, adapter, and lifecycle wiring. zero behavior change. The DX8 path remains the only renderer; g_renderBackend is constructed as a DX8Backend that forwards every method to the existing DX8Wrapper static facade.

What this PR adds

  • IRenderBackend.h — abstract interface covering the W3D-facing subset of DX8Wrapper's public API, plus an optional Initialize/Shutdown device lifecycle pair (no-ops for DX8). Only high-level methods (those taking ShaderClass/TextureBaseClass/Matrix4x4/etc.) are virtualized. D3D8-typed low-level methods stay on DX8Wrapper as DX8Backend-specific escape hatches.
  • Backend/DX8Backend.{h,cpp} — concrete adapter that forwards every virtual method to the existing DX8Wrapper:: static functions. Pure forwarding, no new rendering logic.
  • Backend/RenderBackend.{h,cpp} — exposes the global IRenderBackend* g_renderBackend. Init_Render_Backend() constructs new DX8Backend() from DX8Wrapper::Do_Onetime_Device_Dependent_Inits(); Shutdown_Render_Backend() tears it down from DX8Wrapper::Do_Onetime_Device_Dependent_Shutdowns().

Test plan

  • Windows CI builds pass
  • Game launches and runs a Skirmish round identically (no visual / behavioral difference)

@bobtista bobtista changed the title feat(ww3d2): add IRenderBackend skeleton (Phase 1, DX8 default) feat(ww3d2): add IRenderBackend Interface Apr 17, 2026
@bobtista
bobtista force-pushed the bobtista/feat/render-backend-interface-skeleton branch 2 times, most recently from 5e014bf to e648a3e Compare April 17, 2026 23:43
@TheSuperHackers TheSuperHackers deleted a comment from greptile-apps Bot Apr 17, 2026
@greptile-apps

greptile-apps Bot commented Apr 17, 2026

Copy link
Copy Markdown

Greptile Summary

This PR introduces the IRenderBackend interface as the first step in a planned renderer refactor, adding a DX8Backend adapter that forwards every virtual method to the existing DX8Wrapper static facade and wiring the lifecycle into Do_Onetime_Device_Dependent_Inits/Shutdowns. There is zero behavior change — the DX8 path remains the only renderer and all new methods are one-line trampolines or no-ops.

  • IRenderBackend.h defines the abstract interface with forward-declared W3D types, a TransformKind enum, and RenderBackendViewport struct; Initialize/Shutdown are optional no-ops to allow the DX8 backend to remain device-agnostic.
  • DX8Backend.{h,cpp} implements every pure-virtual via a single-line forwarding call to DX8Wrapper; a To_D3D_Transform helper maps the new enum to D3DTRANSFORMSTATETYPE.
  • RenderBackend.{h,cpp} owns the global g_renderBackend pointer with guarded Init_Render_Backend() / Shutdown_Render_Backend() lifecycle functions; shutdown correctly null-checks before calling Shutdown() and then deletes.

Confidence Score: 5/5

Safe to merge — all new code is either pure forwarding trampolines or no-ops with no behavioral impact on the existing DX8 path.

Every DX8Backend method is a one-line call to the existing DX8Wrapper static function, Initialize/Shutdown are no-ops, the shutdown path has a correct null guard, and CI confirms the game runs identically.

The dx8wrapper.cpp wiring (lines 398-399) is worth revisiting when a real Initialize() implementation is introduced in a future backend.

Important Files Changed

Filename Overview
Core/Libraries/Source/WWVegas/WW3D2/IRenderBackend.h New abstract interface covering the W3D-facing subset of DX8Wrapper's public API; uses #pragma once, forward-declares all types cleanly, defines TransformKind enum and RenderBackendViewport struct, and leaves Initialize/Shutdown as optional no-ops.
Core/Libraries/Source/WWVegas/WW3D2/Backend/DX8Backend.h Concrete adapter header fully overriding all pure-virtual IRenderBackend methods; uses #pragma once and correct TheSuperHackers copyright.
Core/Libraries/Source/WWVegas/WW3D2/Backend/DX8Backend.cpp Pure forwarding trampolines to DX8Wrapper static API; To_D3D_Transform helper correctly maps TransformKind enum to D3D constants.
Core/Libraries/Source/WWVegas/WW3D2/Backend/RenderBackend.cpp Global g_renderBackend owner with guarded Init/Shutdown lifecycle functions; null checks are correct and symmetric.
Core/Libraries/Source/WWVegas/WW3D2/Backend/RenderBackend.h Declares g_renderBackend extern and the two lifecycle functions; clean and minimal.
Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp Wires Init_Render_Backend/Initialize into Do_Onetime_Device_Dependent_Inits and shutdown into Do_Onetime_Device_Dependent_Shutdowns; Initialize() is called unconditionally after the guarded Init_Render_Backend(), which is fine today but fragile for future backends.
Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt Adds Backend/ sources and IRenderBackend.h to WW3D2_SRC in alphabetical order; no issues.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant DX8W as DX8Wrapper
    participant RB as RenderBackend.cpp
    participant DX8B as DX8Backend
    participant IRB as IRenderBackend

    Note over DX8W: Do_Onetime_Device_Dependent_Inits()
    DX8W->>RB: Init_Render_Backend()
    RB->>DX8B: new DX8Backend()
    DX8B-->>RB: g_renderBackend set
    RB-->>DX8W: return
    DX8W->>IRB: "g_renderBackend->Initialize(hwnd, w, h)"
    Note over IRB: no-op in DX8Backend

    Note over DX8W: Per-frame rendering
    DX8W->>IRB: "g_renderBackend->Begin_Scene()"
    IRB->>DX8B: DX8Backend::Begin_Scene()
    DX8B->>DX8W: DX8Wrapper::Begin_Scene()

    Note over DX8W: Do_Onetime_Device_Dependent_Shutdowns()
    DX8W->>IRB: "g_renderBackend->Shutdown() [null-guarded]"
    Note over IRB: no-op in DX8Backend
    DX8W->>RB: Shutdown_Render_Backend()
    RB->>DX8B: delete g_renderBackend
    RB-->>DX8W: "g_renderBackend = nullptr"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant DX8W as DX8Wrapper
    participant RB as RenderBackend.cpp
    participant DX8B as DX8Backend
    participant IRB as IRenderBackend

    Note over DX8W: Do_Onetime_Device_Dependent_Inits()
    DX8W->>RB: Init_Render_Backend()
    RB->>DX8B: new DX8Backend()
    DX8B-->>RB: g_renderBackend set
    RB-->>DX8W: return
    DX8W->>IRB: "g_renderBackend->Initialize(hwnd, w, h)"
    Note over IRB: no-op in DX8Backend

    Note over DX8W: Per-frame rendering
    DX8W->>IRB: "g_renderBackend->Begin_Scene()"
    IRB->>DX8B: DX8Backend::Begin_Scene()
    DX8B->>DX8W: DX8Wrapper::Begin_Scene()

    Note over DX8W: Do_Onetime_Device_Dependent_Shutdowns()
    DX8W->>IRB: "g_renderBackend->Shutdown() [null-guarded]"
    Note over IRB: no-op in DX8Backend
    DX8W->>RB: Shutdown_Render_Backend()
    RB->>DX8B: delete g_renderBackend
    RB-->>DX8W: "g_renderBackend = nullptr"
Loading

Reviews (5): Last reviewed commit: "chore(ww3d2): use qualified include path..." | Re-trigger Greptile

@xezon xezon left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logical approach in an effort to start introducing new render backends. Some open questions.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/RenderBackend.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/RenderBackend.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/IRenderBackend.h Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/RenderBackend.cpp Outdated
// TheSuperHackers @refactor bobtista 10/04/2026 Construct the global
// IRenderBackend instance now that the D3D device is ready. See
// Core/Libraries/Source/WWVegas/WW3D2/RENDER_BACKEND.md.
Init_Render_Backend();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks suspicious. Shouldn't Init_Render_Backend call DX8Wrapper::Do_Onetime_Device_Dependent_Inits instead?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think so. The contract is: DX8Wrapper owns when the renderer comes up (it owns the D3D device), and the backend owns how it brings up its own device. To make that explicit rather than implied, I'll add a lifecycle pair to the interface eg Initialize(hwnd, w, h) / Shutdown() with empty default bodies, called after Init_Render_Backend() and before Shutdown_Render_Backend(). DX8Backend treats them as no-ops since DX8Wrapper still owns the real device, and non-DX8 backends use Initialize() to create their own device/swapchain against the game window. Device-lost/reset stays DX8-internal, it's a D3D8 artifact, and modern backends recover via swapchain reset without surfacing it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about it calls g_renderBackend = new DX8Backend(); directly then? Is Init_Render_Backend needed?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the named Init_Render_Backend() / Shutdown_Render_Backend() pair. It's the single place backend selection lives, so dx8wrapper.cpp includes only RenderBackend.h and not DX8Backend.h. Inlining new DX8Backend() would couple DX8Wrapper to the concrete adapter and push backend-selection into the call site. The pair also gives a symmetric construct/teardown that nulls the pointer on the way out.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp Outdated
Comment thread Core/Libraries/Source/WWVegas/WW3D2/RenderBackend.h Outdated
// Implementations (DX8Backend.cpp, future BgfxBackend.cpp, etc.) can use
// whatever C++ features the project's main build allows.

class IRenderBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How will we cover DX8Caps related stuff?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capability queries become a set of narrow virtuals with safe defaults eg Supports_Texture_Format(WW3DFormat), Supports_Compressed_Textures(), Get_Texture_Limits(), Supports_Texture_Op(...), etc.

DX8Backend forwards each to DX8Wrapper::Get_Current_Caps(), so DX8Caps stays the reference implementation and is never exposed directly. The few sites that read raw D3DCAPS8 bitfields today (COLORWRITEENABLE in W3DScene/W3DVolumetricShadow, TextureOpCaps in shader.cpp) and the Voodoo3 vendor check get promoted to neutral predicates (Supports_Color_Write_Mask(), Supports_Texture_Op(), Is_Legacy_Voodoo3()) so shared engine code carries no D3D types or #ifdefs. I've kept these out of this scaffolding PR deliberately, each lands with the call-site it unblocks, but the shape is good.

// Implementations (DX8Backend.cpp, future BgfxBackend.cpp, etc.) can use
// whatever C++ features the project's main build allows.

class IRenderBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What will we do with direct calls to DX8, for example _Get_D3D_Device8 or Get_DX8_Texture_Stage_State_Value_Name ?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This took a lot of doing, but has been worth it. In my run ahead branches for bgfx (still local), I have it so there are two classes, handled differently. The high-frequency render/texture-stage-state calls aren't re-exposed as raw D3DRS_/D3DTSS_; they route through a backend-neutral fixed-function state cache (keyed by the same ordinals) plus typed semantic setters, so a non-DX8 backend reads intent rather than D3D enums. The genuinely DX8-only entry points (_Get_D3D_Device8, Create_DX8*, raw SetRenderTarget) migrate to named high-level methods (Set_Render_Target_With_Z, a view-capture primitive, …); the irreducible cases e.g. hand-written water pixel-shader bytecode go behind a named-enum hatch (Create_Legacy_Pixel_Shader(kind)) rather than a raw device pointer. The end state has no _Get_D3D_Device8 left in the engine subsystems; the raw device stays inside DX8Backend. Pure DX8 diagnostics eg Get_DX8_Texture_Stage_State_Value_Name simply stay on DX8Wrapper, as they're debug-only and not part of the abstraction.

Comment thread Core/Libraries/Source/WWVegas/WW3D2/CMakeLists.txt Outdated
@bobtista

Copy link
Copy Markdown
Author

I've already got the bgfx backend working on my Mac and Windows machines, a lot was figured along the way, and some stuff like the lifecycle I'm happy to add here and inherit from. I'm still ironing out some quirks, will share more when things feel polished enough.

Comment on lines +460 to +461
g_renderBackend->Shutdown();
Shutdown_Render_Backend();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 g_renderBackend->Shutdown() is called unconditionally before Shutdown_Render_Backend(), which contains the null guard. If Do_Onetime_Device_Dependent_Shutdowns() is ever reached without a paired Do_Onetime_Device_Dependent_Inits() — for example through an error-recovery path that resets the device partway through initialization — this is a null-pointer dereference crash. The guard should wrap both calls.

Suggested change
g_renderBackend->Shutdown();
Shutdown_Render_Backend();
if (g_renderBackend != nullptr)
{
g_renderBackend->Shutdown();
}
Shutdown_Render_Backend();
Prompt To Fix With AI
This is a comment left during a code review.
Path: Core/Libraries/Source/WWVegas/WW3D2/dx8wrapper.cpp
Line: 460-461

Comment:
`g_renderBackend->Shutdown()` is called unconditionally before `Shutdown_Render_Backend()`, which contains the null guard. If `Do_Onetime_Device_Dependent_Shutdowns()` is ever reached without a paired `Do_Onetime_Device_Dependent_Inits()` — for example through an error-recovery path that resets the device partway through initialization — this is a null-pointer dereference crash. The guard should wrap both calls.

```suggestion
	if (g_renderBackend != nullptr)
	{
		g_renderBackend->Shutdown();
	}
	Shutdown_Render_Backend();
```

How can I resolve this? If you propose a fix, please make it concise.

distlod.cpp
distlod.h
dllist.h
Backend/DX8Backend.cpp

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be sorted alphabetically

// Method names intentionally match the existing DX8Wrapper names so migrating a
// caller is a mechanical DX8Wrapper::X(...) -> g_renderBackend->X(...) rewrite.

class IRenderBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about the following functions? How will they be dealt with if not as part of the IRenderBackend? Does the IRenderBackend already claim to be complete or are the chosen function just the simple ones that can be abstracted so far?

SetCleanupHook
Is_Initted
Get_Format_Name
Get_Render_State
Set_Render_State
Release_Render_State
Get_Free_Texture_RAM
Begin_Statistics
End_Statistics
Get_Last_Frame_Statistics
Get_FrameCount
Get_Fog_Color
Convert_Color (looks like utility function to be moved elsewhere)
Clamp_Color (looks like utility function to be moved elsewhere)
Set_Alpha (looks like utility function to be moved elsewhere)
Create_Additional_Swap_Chain
Set_Render_Target
Apply_Default_State
Get_Vertex_Processing_Behavior
getBackBufferFormat
Reset_Device
Registry_Save_Render_Device
Registry_Load_Render_Device
Set_Draw_Polygon_Low_Bound_Limit

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left it minimalistic here on purpose, the idea is that in future PRs we add each method as needed for a given backend eg bgfx. I'm also happy to add them here if that's better. Could split them into buckets like so:

  • Will be promoted when a backend needs them: render-state, statistics, Reset_Device, Set_Render_Target (skeleton already has Set_Render_Target_With_Z + Create_Render_Target).
  • Stay DX8-specific (escape hatches on DX8Wrapper): _Get_D3D_Device8, format-name/state-name debug helpers, registry device save/load, Get_Vertex_Processing_Behavior.
  • Don't belong on a backend at all (utilities): Convert_Color, Clamp_Color, Set_Alpha — agree with him, separate cleanup.

// Method names intentionally match the existing DX8Wrapper names so migrating a
// caller is a mechanical DX8Wrapper::X(...) -> g_renderBackend->X(...) rewrite.

class IRenderBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will there be a change to cleanup DX8Wrapper? It looks like it contains a number of things that do not directly belong there, such as Convert_Color, Clamp_Color.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, would you rather that happen in this PR vs a separate one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll do that as a separate PR, keeps this one reviewable

// TheSuperHackers @refactor bobtista 10/04/2026 Construct the global
// IRenderBackend instance now that the D3D device is ready. See
// Core/Libraries/Source/WWVegas/WW3D2/RENDER_BACKEND.md.
Init_Render_Backend();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about it calls g_renderBackend = new DX8Backend(); directly then? Is Init_Render_Backend needed?


// The active rendering backend. Set by Init_Render_Backend() and cleared by
// Shutdown_Render_Backend(); never null between those two calls.
extern IRenderBackend * g_renderBackend;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this perhaps be part of WW3D class ? It seems like it fits there. Basically WW3D already is a high level entry point to and owner of the rendering ecosystem. See WW3D Static Globals in ww3d.cpp

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like this idea. Want it done in this PR or a separate one?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WW3D isn't in Core though, it's still in Generals/ and GeneralsMD/, while RenderBackend/IRenderBackend/DX8Wrapper are shared Core code. For layering and ordering this is a bit messier until we unify it. What do you think?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's keep the free functions here for now, and fold ownership into WW3D once WW3D is unified into Core. I'd rather not layer Core code on a header that still lives in both game trees

// Method names intentionally match the existing DX8Wrapper names so migrating a
// caller is a mechanical DX8Wrapper::X(...) -> g_renderBackend->X(...) rewrite.

class IRenderBackend

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will IRenderBackend then also be served with static functions through WW3D class? It already does so for a number of 1 to 1 DX8Wrapper function calls, such as

void WW3D::Flip_To_Primary()
{
	DX8Wrapper::Flip_To_Primary();
}

void WW3D::Set_Gamma(float gamma,float bright,float contrast,bool calibrate)
{
	DX8Wrapper::Set_Gamma(gamma,bright,contrast,calibrate);
}

@xezon xezon added Refactor Edits the code with insignificant behavior changes, is never user facing Rendering Is Rendering related labels Jun 4, 2026
@xezon xezon changed the title feat(ww3d2): add IRenderBackend Interface chore(ww3d2): add IRenderBackend Interface Jun 4, 2026
@bobtista
bobtista force-pushed the bobtista/feat/render-backend-interface-skeleton branch from a3d0d92 to a93385d Compare July 19, 2026 09:57
@bobtista

Copy link
Copy Markdown
Author

Rebased onto main and updated, addressed comments. Since the last review, I've been using a complete bgfx backend running behind this interface on both Windows and macOS, so the open questions above have practical answers now. The interface had state setters promoted as callers needed them, DX8-only entry points stayed on DX8Wrapper, the color utilities never belonged on a backend.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Refactor Edits the code with insignificant behavior changes, is never user facing Rendering Is Rendering related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants