Skip to content

Commit efbf189

Browse files
stephanmeestersxezon
authored andcommitted
feat(profiling): Add frame image capturing to Tracy profiling (TheSuperHackers#2202)
1 parent 8a604fa commit efbf189

7 files changed

Lines changed: 342 additions & 0 deletions

File tree

Core/GameEngineDevice/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ set(GAMEENGINEDEVICE_SRC
5656
Include/W3DDevice/GameClient/W3DMouse.h
5757
# Include/W3DDevice/GameClient/W3DParticleSys.h
5858
# Include/W3DDevice/GameClient/W3DPoly.h
59+
Include/W3DDevice/GameClient/W3DProfilerFrameCapture.h
5960
# Include/W3DDevice/GameClient/W3DProjectedShadow.h
6061
Include/W3DDevice/GameClient/W3DPropBuffer.h
6162
# Include/W3DDevice/GameClient/W3DRoadBuffer.h
@@ -159,6 +160,7 @@ set(GAMEENGINEDEVICE_SRC
159160
Source/W3DDevice/GameClient/W3DMouse.cpp
160161
# Source/W3DDevice/GameClient/W3DParticleSys.cpp
161162
# Source/W3DDevice/GameClient/W3DPoly.cpp
163+
Source/W3DDevice/GameClient/W3DProfilerFrameCapture.cpp
162164
Source/W3DDevice/GameClient/W3DPropBuffer.cpp
163165
# Source/W3DDevice/GameClient/W3DRoadBuffer.cpp
164166
# Source/W3DDevice/GameClient/W3DScene.cpp
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2026 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#pragma once
20+
21+
#ifdef PROFILER_ENABLED
22+
23+
#include "Lib/BaseType.h"
24+
#include <vector>
25+
26+
class W3DProfilerFrameCapture
27+
{
28+
public:
29+
W3DProfilerFrameCapture();
30+
~W3DProfilerFrameCapture();
31+
32+
void Capture(UnsignedInt displayWidth, UnsignedInt displayHeight);
33+
34+
private:
35+
bool ShouldReuseLastCapture(UnsignedInt currentTimeMs) const;
36+
37+
DWORD m_swizzleShader = 0;
38+
UnsignedInt m_lastCaptureTimeMs = 0;
39+
UnsignedInt m_lastCaptureHeight = 0;
40+
std::vector<UnsignedByte> m_lastCapturePixels;
41+
};
42+
43+
#endif // PROFILER_ENABLED
Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
** Command & Conquer Generals Zero Hour(tm)
3+
** Copyright 2026 TheSuperHackers
4+
**
5+
** This program is free software: you can redistribute it and/or modify
6+
** it under the terms of the GNU General Public License as published by
7+
** the Free Software Foundation, either version 3 of the License, or
8+
** (at your option) any later version.
9+
**
10+
** This program is distributed in the hope that it will be useful,
11+
** but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
** GNU General Public License for more details.
14+
**
15+
** You should have received a copy of the GNU General Public License
16+
** along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
#ifdef PROFILER_ENABLED
20+
21+
#include "../../../Include/W3DDevice/GameClient/W3DProfilerFrameCapture.h"
22+
23+
#include "WW3D2/dx8wrapper.h"
24+
#include "WW3D2/surfaceclass.h"
25+
#include "WW3D2/texture.h"
26+
#include "WW3D2/ww3d.h"
27+
#include "WW3D2/ww3dformat.h"
28+
#include "WWMath/wwmath.h"
29+
#include <cstring>
30+
#include <d3dx8core.h>
31+
32+
W3DProfilerFrameCapture::W3DProfilerFrameCapture()
33+
{
34+
}
35+
36+
W3DProfilerFrameCapture::~W3DProfilerFrameCapture()
37+
{
38+
if (m_swizzleShader)
39+
{
40+
DX8Wrapper::_Get_D3D_Device8()->DeletePixelShader(m_swizzleShader);
41+
m_swizzleShader = 0;
42+
}
43+
}
44+
45+
bool W3DProfilerFrameCapture::ShouldReuseLastCapture(UnsignedInt currentTimeMs) const
46+
{
47+
return PROFILER_FRAME_IMAGE_INTERVAL_MS > 0
48+
&& currentTimeMs - m_lastCaptureTimeMs < PROFILER_FRAME_IMAGE_INTERVAL_MS
49+
&& !m_lastCapturePixels.empty();
50+
}
51+
52+
void W3DProfilerFrameCapture::Capture(UnsignedInt displayWidth, UnsignedInt displayHeight)
53+
{
54+
if (!PROFILER_IS_CONNECTED)
55+
return;
56+
57+
// the profiler expects an image every render frame. resend the last capture if we're inside the capture interval.
58+
const UnsignedInt currentTimeMs = WW3D::Get_Logic_Time_Milliseconds();
59+
if (ShouldReuseLastCapture(currentTimeMs))
60+
{
61+
PROFILER_FRAME_IMAGE(m_lastCapturePixels.data(), PROFILER_FRAME_IMAGE_SIZE, m_lastCaptureHeight, 0, false);
62+
return;
63+
}
64+
65+
// compile swizzle shader convert BGRA to RGBA
66+
// TheSuperHackers @todo In DX9 with ps2.0 this shader will be much simpler
67+
if (!m_swizzleShader)
68+
{
69+
ID3DXBuffer *compiledShader = nullptr;
70+
const char *shader =
71+
"ps.1.4\n"
72+
"texld r0, t0\n"
73+
"mov r1.a, r0.r\n"
74+
"mov r2.a, r0.g\n"
75+
"mov r3.a, r0.b\n"
76+
"mul r0.rgb, r3.a, c0\n"
77+
"mad r0.rgb, r2.a, c1, r0\n"
78+
"mad r0.rgb, r1.a, c2, r0\n";
79+
80+
HRESULT hr = D3DXAssembleShader(shader, strlen(shader), 0, nullptr, &compiledShader, nullptr);
81+
if (FAILED(hr))
82+
return;
83+
84+
hr = DX8Wrapper::_Get_D3D_Device8()->CreatePixelShader((DWORD *)compiledShader->GetBufferPointer(), &m_swizzleShader);
85+
compiledShader->Release();
86+
87+
if (FAILED(hr))
88+
return;
89+
}
90+
91+
// allocate render target
92+
TextureClass *renderTarget = DX8Wrapper::Create_Render_Target(PROFILER_FRAME_IMAGE_SIZE, PROFILER_FRAME_IMAGE_SIZE, WW3D_FORMAT_A8R8G8B8);
93+
if (!renderTarget)
94+
return;
95+
96+
// allocate surface class
97+
const Real aspectRatio = (Real)displayHeight / (Real)displayWidth;
98+
unsigned int profilerImageHeight = min((int)WWMath::Round(PROFILER_FRAME_IMAGE_SIZE * aspectRatio), PROFILER_FRAME_IMAGE_SIZE);
99+
SurfaceClass *surfaceClass = NEW_REF(SurfaceClass, (PROFILER_FRAME_IMAGE_SIZE, profilerImageHeight, WW3D_FORMAT_A8R8G8B8));
100+
if (!surfaceClass)
101+
{
102+
REF_PTR_RELEASE(renderTarget);
103+
return;
104+
}
105+
106+
// get the backbuffer
107+
SurfaceClass *backBuffer = DX8Wrapper::_Get_DX8_Back_Buffer();
108+
if (!backBuffer)
109+
{
110+
REF_PTR_RELEASE(surfaceClass);
111+
REF_PTR_RELEASE(renderTarget);
112+
return;
113+
}
114+
115+
IDirect3DSurface8 *backBufferSurface = backBuffer->Peek_D3D_Surface();
116+
D3DSURFACE_DESC backBufferSurfaceDesc;
117+
HRESULT hr = backBufferSurface->GetDesc(&backBufferSurfaceDesc);
118+
if (FAILED(hr))
119+
{
120+
REF_PTR_RELEASE(backBuffer);
121+
REF_PTR_RELEASE(surfaceClass);
122+
REF_PTR_RELEASE(renderTarget);
123+
return;
124+
}
125+
126+
// allocate intermediate texture
127+
IDirect3DTexture8 *intermediateTexture = nullptr;
128+
hr = DX8Wrapper::_Get_D3D_Device8()->CreateTexture(
129+
backBufferSurfaceDesc.Width,
130+
backBufferSurfaceDesc.Height,
131+
1,
132+
D3DUSAGE_RENDERTARGET,
133+
backBufferSurfaceDesc.Format,
134+
D3DPOOL_DEFAULT,
135+
&intermediateTexture);
136+
if (FAILED(hr))
137+
{
138+
REF_PTR_RELEASE(backBuffer);
139+
REF_PTR_RELEASE(surfaceClass);
140+
REF_PTR_RELEASE(renderTarget);
141+
return;
142+
}
143+
144+
// draw backbuffer to intermediate texture
145+
IDirect3DSurface8 *intermediateTextureSurface;
146+
hr = intermediateTexture->GetSurfaceLevel(0, &intermediateTextureSurface);
147+
if (FAILED(hr))
148+
{
149+
REF_PTR_RELEASE(backBuffer);
150+
REF_PTR_RELEASE(surfaceClass);
151+
REF_PTR_RELEASE(renderTarget);
152+
intermediateTexture->Release();
153+
return;
154+
}
155+
DX8Wrapper::_Copy_DX8_Rects(backBufferSurface, nullptr, 0, intermediateTextureSurface, nullptr);
156+
intermediateTextureSurface->Release();
157+
intermediateTextureSurface = nullptr;
158+
159+
// release the backbuffer
160+
backBufferSurface = nullptr;
161+
REF_PTR_RELEASE(backBuffer);
162+
163+
// set render target to a small surface
164+
IDirect3DSurface8 *smallRenderTargetSurface = renderTarget->Get_D3D_Surface_Level();
165+
WWASSERT(smallRenderTargetSurface != nullptr);
166+
DX8Wrapper::Set_Render_Target(smallRenderTargetSurface, false);
167+
168+
// set viewport
169+
IDirect3DDevice8 *device = DX8Wrapper::_Get_D3D_Device8();
170+
D3DVIEWPORT8 restoreViewport;
171+
device->GetViewport(&restoreViewport);
172+
173+
SurfaceClass::SurfaceDescription smallRenderDesc;
174+
surfaceClass->Get_Description(smallRenderDesc);
175+
176+
D3DVIEWPORT8 viewport;
177+
viewport.X = 0;
178+
viewport.Y = 0;
179+
viewport.Width = PROFILER_FRAME_IMAGE_SIZE;
180+
viewport.Height = smallRenderDesc.Height;
181+
viewport.MinZ = 0.0f;
182+
viewport.MaxZ = 1.0f;
183+
DX8Wrapper::Set_Viewport(&viewport);
184+
185+
// bind swizzle shader
186+
DX8Wrapper::Set_Pixel_Shader(m_swizzleShader);
187+
static const Real kMaskR[4] = {1.0f, 0.0f, 0.0f, 0.0f};
188+
static const Real kMaskG[4] = {0.0f, 1.0f, 0.0f, 0.0f};
189+
static const Real kMaskB[4] = {0.0f, 0.0f, 1.0f, 0.0f};
190+
device->SetPixelShaderConstant(0, kMaskR, 1);
191+
device->SetPixelShaderConstant(1, kMaskG, 1);
192+
device->SetPixelShaderConstant(2, kMaskB, 1);
193+
194+
// draw texture scaled-down onto a small surface
195+
struct QuadVertex
196+
{
197+
Real x, y, z, rhw;
198+
Real u, v;
199+
} vtx[4];
200+
const Real left = -0.5f;
201+
const Real top = -0.5f;
202+
const Real right = (Real)PROFILER_FRAME_IMAGE_SIZE - 0.5f;
203+
const Real bottom = (Real)smallRenderDesc.Height - 0.5f;
204+
vtx[0] = {right, bottom, 0.0f, 1.0f, 1.0f, 1.0f};
205+
vtx[1] = {right, top, 0.0f, 1.0f, 1.0f, 0.0f};
206+
vtx[2] = {left, bottom, 0.0f, 1.0f, 0.0f, 1.0f};
207+
vtx[3] = {left, top, 0.0f, 1.0f, 0.0f, 0.0f};
208+
DX8Wrapper::Set_DX8_Texture(0, intermediateTexture);
209+
DX8Wrapper::Set_Vertex_Shader(D3DFVF_XYZRHW | D3DFVF_TEX1);
210+
device->DrawPrimitiveUP(D3DPT_TRIANGLESTRIP, 2, vtx, sizeof(QuadVertex));
211+
DX8Wrapper::Set_Pixel_Shader(0);
212+
DX8Wrapper::Set_DX8_Texture(0, nullptr);
213+
DX8Wrapper::Set_Viewport(&restoreViewport);
214+
DX8Wrapper::Set_Render_Target(static_cast<IDirect3DSurface8 *>(nullptr));
215+
216+
// copy the small surface pixels from GPU to CPU
217+
RECT srcRect = { 0, 0, PROFILER_FRAME_IMAGE_SIZE, smallRenderDesc.Height };
218+
POINT dstPoint = { 0, 0 };
219+
DX8Wrapper::_Copy_DX8_Rects(
220+
smallRenderTargetSurface,
221+
&srcRect,
222+
1,
223+
surfaceClass->Peek_D3D_Surface(),
224+
&dstPoint);
225+
smallRenderTargetSurface->Release();
226+
227+
// send pixels to the profiler backend
228+
int pitch = 0;
229+
void *bits = surfaceClass->Lock(&pitch);
230+
if (bits)
231+
{
232+
const size_t rowBytes = (size_t)PROFILER_FRAME_IMAGE_SIZE * 4;
233+
m_lastCaptureHeight = smallRenderDesc.Height;
234+
m_lastCapturePixels.resize(rowBytes * m_lastCaptureHeight);
235+
236+
const UnsignedByte *source = static_cast<const UnsignedByte *>(bits);
237+
UnsignedByte *destination = m_lastCapturePixels.data();
238+
for (UnsignedInt row = 0; row < m_lastCaptureHeight; ++row)
239+
{
240+
std::memcpy(destination + row * rowBytes, source + row * pitch, rowBytes);
241+
}
242+
243+
PROFILER_FRAME_IMAGE(m_lastCapturePixels.data(), PROFILER_FRAME_IMAGE_SIZE, m_lastCaptureHeight, 0, false);
244+
surfaceClass->Unlock();
245+
m_lastCaptureTimeMs = currentTimeMs;
246+
}
247+
248+
// cleanup
249+
intermediateTexture->Release();
250+
intermediateTexture = nullptr;
251+
REF_PTR_RELEASE(surfaceClass);
252+
REF_PTR_RELEASE(renderTarget);
253+
}
254+
255+
#endif // PROFILER_ENABLED

Generals/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include "GameClient/Display.h"
3737
#include "WW3D2/lightenvironment.h"
38+
#include "W3DDevice/GameClient/W3DProfilerFrameCapture.h"
3839

3940
class VideoBuffer;
4041
class W3DDebugDisplay;
@@ -183,6 +184,10 @@ class W3DDisplay : public Display
183184
Int64 m_timerAtCumuFPSStart;
184185
#endif
185186

187+
#ifdef PROFILER_ENABLED
188+
W3DProfilerFrameCapture *m_profilerFrameCapture;
189+
#endif
190+
186191
enum
187192
{
188193
FPS, ///< debug display frames per second

Generals/Code/GameEngineDevice/Source/W3DDevice/GameClient/W3DDisplay.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ static void drawFramerateBar();
7070
#include "W3DDevice/GameClient/W3DGameClient.h"
7171
#include "W3DDevice/GameClient/W3DFileSystem.h"
7272
#include "W3DDevice/GameClient/W3DDynamicLight.h"
73+
#include "W3DDevice/GameClient/W3DProfilerFrameCapture.h"
7374
#include "W3DDevice/GameClient/HeightMap.h"
7475
#include "W3DDevice/GameClient/WorldHeightMap.h"
7576
#include "W3DDevice/GameClient/W3DScene.h"
@@ -358,13 +359,21 @@ W3DDisplay::W3DDisplay()
358359
m_batchMode = DRAW_IMAGE_ALPHA;
359360
m_batchGrayscale = FALSE;
360361
m_batchNeedsInit = FALSE;
362+
363+
#ifdef PROFILER_ENABLED
364+
m_profilerFrameCapture = NEW W3DProfilerFrameCapture();
365+
#endif
361366
}
362367

363368
// W3DDisplay::~W3DDisplay ====================================================
364369
/** */
365370
//=============================================================================
366371
W3DDisplay::~W3DDisplay()
367372
{
373+
#ifdef PROFILER_ENABLED
374+
delete m_profilerFrameCapture;
375+
m_profilerFrameCapture = nullptr;
376+
#endif
368377

369378
// get rid of the debug display
370379
delete m_debugDisplay;
@@ -1968,6 +1977,13 @@ void W3DDisplay::draw()
19681977
TheGraphDraw->render();
19691978
TheGraphDraw->clear();
19701979
#endif
1980+
1981+
#ifdef PROFILER_ENABLED
1982+
if (m_profilerFrameCapture && !TheGlobalData->m_headless)
1983+
{
1984+
m_profilerFrameCapture->Capture(getWidth(), getHeight());
1985+
}
1986+
#endif
19711987
// render is all done!
19721988
WW3D::End_Render();
19731989
}

GeneralsMD/Code/GameEngineDevice/Include/W3DDevice/GameClient/W3DDisplay.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535

3636
#include "GameClient/Display.h"
3737
#include "WW3D2/lightenvironment.h"
38+
#include "W3DDevice/GameClient/W3DProfilerFrameCapture.h"
3839

3940
class VideoBuffer;
4041
class W3DDebugDisplay;
@@ -183,6 +184,10 @@ class W3DDisplay : public Display
183184
Int64 m_timerAtCumuFPSStart;
184185
#endif
185186

187+
#ifdef PROFILER_ENABLED
188+
W3DProfilerFrameCapture *m_profilerFrameCapture;
189+
#endif
190+
186191
enum
187192
{
188193
FPS, ///< debug display frames per second

0 commit comments

Comments
 (0)