This is the write-up for Assignment 04 of GAMES 6320 (Game Engineering II). Assignment 03 introduced cMesh/cEffect abstractions and a platform-independent Graphics.cpp. Assignment 04 turns cMesh and cEffect into reference-counted assets, adds SubmitMeshWithEffect / SubmitClearColor submission interfaces, and moves mesh/effect ownership from the Graphics project to the MyGame project.


Download

Release build (Direct3D x64): MyGame_Assignment04_Release_x64.zip

The ZIP contains a Direct3D Release build (x64). Unzip it and run MyGame.exe.
Controls: hold H to hide the rectangle; hold A to switch the triangle to the animated effect; press Esc to close the window.


1. Overview

On top of Assignment 03, this assignment completes the following core work:

  1. Reference-counted resources: cMesh and cEffect become reference-counted objects using the macros in ReferenceCountedAssets.h, and provide static Load() factory functions.
  2. Submission interfaces: the Graphics project gains two interfaces — SubmitMeshWithEffect(cMesh&, cEffect&) and SubmitClearColor(sColor) — for the application to call on the submit thread.
  3. Frame data caching: submitted mesh/effect pairs are cached in a fixed-size array with zero runtime dynamic allocation.
  4. Ownership migration: the two meshes (rectangle, triangle) and two effects (standard, animated) are created, submitted, and cleaned up by MyGame; the Graphics project no longer owns any mesh/effect.
  5. Keyboard interaction: hold H to hide the rectangle; hold A to make the triangle use the animated effect.
  6. ExampleGame: still runs, but shows solid black with no triangles (the default clear color is black, and it submits nothing).

The project is a standalone copy of Assignment 03 (the original Assignment 03 is preserved for rollback and was not modified further). Build configurations:

Platform (sln) Project level Graphics API Pointer size
x64 x64 Direct3D 8 bytes
x86 Win32 OpenGL 4 bytes

2. Modified Files

File Change
Engine/Graphics/cMesh.h / cMesh.cpp Turned into a reference-counted resource; added a static Load() factory
Engine/Graphics/cEffect.h / cEffect.cpp Turned into a reference-counted resource; added a static Load() factory
Engine/Graphics/Graphics.h Forward-declared cMesh/cEffect; declared SubmitMeshWithEffect
Engine/Graphics/Graphics.cpp Default clear color changed to black; frame data struct gained clearColor + a mesh/effect pair array; implemented SubmitMeshWithEffect, cleanup, and rendering
MyGame_/MyGame/cMyGame.h Added 4 resource pointers + 2 boolean flags; declared the relevant overrides
MyGame_/MyGame/cMyGame.cpp Creates / submits / cleans up the two meshes and two effects; handles keys
ExampleGame_/ExampleGame/ No changes needed (only handles ESC to exit; shows solid black)

3. Reference-Counted Resources (cMesh / cEffect)

Both classes become reference-counted resources via the macros in ReferenceCountedAssets.h:

class cMesh
{
public:
void Draw();
static cResult Load( const sMeshData& i_meshData, cMesh*& o_mesh );

EAE6320_ASSETS_DECLAREDELETEDREFERENCECOUNTEDFUNCTIONS( cMesh ); // no copy/move
EAE6320_ASSETS_DECLAREREFERENCECOUNTINGFUNCTIONS(); // Increment / Decrement

private:
unsigned int m_vertexCount = 0;
unsigned int m_indexCount = 0;
EAE6320_ASSETS_DECLAREREFERENCECOUNT(); // uint16_t m_referenceCount = 1;
// ... platform-specific resources (D3D pointers / GL ids) ...
cMesh() = default;
~cMesh(); // asserts the count is zero on destruction
};

Key macro behavior (ReferenceCountedAssets.h):

  • IncrementReferenceCount() / DecrementReferenceCount() use InterlockedIncrement16 / InterlockedDecrement16 (thread-safe atomic operations); when the count reaches 0, delete this is executed — i.e. the last referencer is responsible for freeing.
  • The reference count m_referenceCount is a uint16_t, deliberately placed after the other small integers / between the shader pointers and the render state (whether it actually fits into padding depends on the platform; see §7.3 for the measured analysis).

Load() uses the factory pattern: on success it returns a cResult and hands back a fresh object with reference count 1 through the output parameter o_mesh/o_effect; on failure it cleans up the partially-built object with a cScopeGuard.

4. Frame Data Caching and Submit Interfaces (Graphics.cpp)

4.1 Frame data structure

constexpr uint16_t s_maxMeshEffectPairCount = 100;

struct sDataRequiredToRenderAFrame
{
ConstantBufferFormats::sFrame constantData_frame;
sColor clearColor = s_defaultClearColor; // black by default

struct sMeshEffectPair
{
cMesh* mesh = nullptr;
cEffect* effect = nullptr;
};
uint16_t meshEffectPairCount = 0;
sMeshEffectPair meshEffectPairs[s_maxMeshEffectPairCount]; // fixed size, zero allocation
};
sDataRequiredToRenderAFrame s_dataRequiredToRenderAFrame[2]; // double buffered

4.2 Submit interfaces

void Graphics::SubmitClearColor( const sColor& i_clearColor )
{
s_dataBeingSubmittedByApplicationThread->clearColor = i_clearColor;
}

void Graphics::SubmitMeshWithEffect( cMesh& i_mesh, cEffect& i_effect )
{
auto& frameData = *s_dataBeingSubmittedByApplicationThread;
if ( frameData.meshEffectPairCount >= s_maxMeshEffectPairCount ) // over budget
{
EAE6320_ASSERTF( false, "Too many mesh/effect pairs (max %u)", ... );
Logging::OutputError( "Too many mesh/effect pairs; the extra pair was ignored" );
return;
}
auto& pair = frameData.meshEffectPairs[frameData.meshEffectPairCount++];
pair.mesh = &i_mesh;
pair.effect = &i_effect;
pair.mesh->IncrementReferenceCount(); // hold a temporary reference on submission
pair.effect->IncrementReferenceCount();
}

4.3 Rendering and cleanup

void Graphics::RenderFrame()
{
// 1. Wait for the application thread to finish, then swap the double-buffer pointers
std::swap( s_dataBeingSubmittedByApplicationThread, s_dataBeingRenderedByRenderThread );

// 2. Clear the back buffer with the submitted clear color
sContext::g_context.ClearBackBuffer( dataRequiredToRenderFrame->clearColor );

// 3. Update the frame constant buffer
s_constantBuffer_frame.Update( &dataRequiredToRenderFrame->constantData_frame );

// 4. Bind each effect and draw its mesh in turn
for ( uint16_t i = 0; i < dataRequiredToRenderFrame->meshEffectPairCount; ++i )
{
meshEffectPair.effect->Bind();
meshEffectPair.mesh->Draw();
}

// 5. Present
sContext::g_context.Present();

// 6. Release this frame's temporary references on the meshes/effects
CleanUpSubmittedMeshEffectPairs( *dataRequiredToRenderFrame );
}

CleanUp() clears both s_dataRequiredToRenderAFrame[0] and [1], to cover the copy that the application thread submitted but that was never rendered.

5. MyGame: Create, Submit, Clean Up

5.1 Initialize (create two effects + two meshes)

cResult cMyGame::Initialize()
{
uint8_t renderStateBits = 0;
RenderStates::EnableDepthTesting( renderStateBits );
RenderStates::EnableDepthWriting( renderStateBits );
RenderStates::EnableDrawingBothTriangleSides( renderStateBits );

// Two effects: standard (solid white) + animated (changes color over time)
Graphics::cEffect::Load( { s_path_vertexShader, s_path_fragmentShader_standard, renderStateBits }, m_effect_standard );
Graphics::cEffect::Load( { s_path_vertexShader, s_path_fragmentShader_animated, renderStateBits }, m_effect_animated );

// Two meshes: rectangle (left) + triangle (right)
Graphics::cMesh::Load( rectangleData, m_mesh_rectangle );
Graphics::cMesh::Load( triangleData, m_mesh_triangle );
}

5.2 Key handling

void cMyGame::UpdateSimulationBasedOnInput()
{
m_shouldRectangleBeDrawn = !UserInput::IsKeyPressed( 'H' ); // hold H to hide the rectangle
m_shouldTriangleUseAnimatedEffect = UserInput::IsKeyPressed( 'A' ); // hold A for the animated effect
}

5.3 Submit (every frame)

void cMyGame::SubmitDataToBeRendered( float, float )
{
Graphics::SubmitClearColor( s_backgroundColor ); // non-black blue background (0, 0, 0.25)

if ( m_shouldRectangleBeDrawn )
Graphics::SubmitMeshWithEffect( *m_mesh_rectangle, *m_effect_standard );

auto* const effect = m_shouldTriangleUseAnimatedEffect ? m_effect_animated : m_effect_standard;
Graphics::SubmitMeshWithEffect( *m_mesh_triangle, *effect );
}

5.4 Clean up

cResult cMyGame::CleanUp()
{
m_mesh_rectangle->DecrementReferenceCount(); // release the game's references
m_mesh_triangle->DecrementReferenceCount();
m_effect_standard->DecrementReferenceCount();
m_effect_animated->DecrementReferenceCount();
// null them out ...
}

6. Screenshots and Verification

Three 512×512 client-area screenshots were captured with a system tool (PrintWindow, which reads the game window’s own rendered contents directly, so occlusion by other windows doesn’t affect them):

Screenshot State Background Rectangle (left) Triangle (right)
screenshot_1_default.png default blue (0,0,64) white (255,255,255) white (255,255,255)
screenshot_2_hide_mesh.png holding H blue (0,0,64) blue (hidden) white (255,255,255)
screenshot_3_different_effect.png holding A blue (0,0,64) white (255,255,255) dark green (animated) (0,67,21)

Default

Holding H hides the rectangle

Holding A switches the triangle effect

All three states were verified pixel-by-pixel to match expectations:

  1. Default: both meshes are drawn with the standard (white) effect on a non-black blue background — proving SubmitClearColor and SubmitMeshWithEffect work.
  2. Holding H: the rectangle area becomes the blue background — proving the rectangle is no longer submitted.
  3. Holding A: the triangle changes from white to dark green (the animated effect changes color over time) — proving the same mesh uses a different effect.

7. sizeof(cMesh) / sizeof(cEffect) and Memory Budget

The numbers below were measured with the compiler: a small block of code writing sizeof(...) to a file was temporarily added to Graphics::Initialize(), built and run on both platforms, and the results were read back (the temporary code was removed afterwards):

Item x64 / Direct3D x86 / OpenGL
sizeof(cMesh) 40 bytes 24 bytes
sizeof(cEffect) 56 bytes 16 bytes
sizeof(sDataRequiredToRenderAFrame) 1768 bytes 964 bytes

7.1 cMesh members and layout

Data members (in declaration order): unsigned int m_vertexCount, unsigned int m_indexCount, uint16_t m_referenceCount (reference count), and three platform handles (D3D: 3 pointers / GL: 3 GLuints).

x64 / D3D (8-byte pointers, 8-byte alignment):

Offset Member Size
0 m_vertexCount 4
4 m_indexCount 4
8 m_referenceCount 2
10–15 padding (align to 8) 6
16 m_vertexFormat* 8
24 m_vertexBuffer* 8
32 m_indexBuffer* 8
— Total 40

x86 / GL (4-byte GLuint, 4-byte alignment):

Offset Member Size
0 m_vertexCount 4
4 m_indexCount 4
8 m_referenceCount 2
10–11 padding (align to 4) 2
12 m_vertexBufferId 4
16 m_indexBufferId 4
20 m_vertexArrayId 4
— Total 24

7.2 cEffect members and layout

Data members: cShader* m_vertexShader, cShader* m_fragmentShader, uint16_t m_referenceCount, cRenderState m_renderState, plus GLuint m_programId on GL. cRenderState holds 3 state pointers + a 1-byte uint8_t m_bits on D3D (32 bytes total), but only a 1-byte m_bits on GL.

x64 / D3D (8-byte alignment):

Offset Member Size
0 m_vertexShader* 8
8 m_fragmentShader* 8
16 m_referenceCount 2
18–23 padding (align to 8) 6
24–55 m_renderState (3 pointers + 1 byte) 32
— Total 56

x86 / GL (4-byte alignment):

Offset Member Size
0 m_vertexShader* 4
4 m_fragmentShader* 4
8 m_referenceCount 2
10 m_renderState.m_bits 1
11 padding (align to 4) 1
12 m_programId 4
— Total 16

7.3 Does the reference count “fill padding”?

The comment in ReferenceCountedAssets.h claims the reference count can be placed anywhere in the class to “fill padding that would otherwise be wasted”. Measurement shows this is only true in some cases:

Class / platform Without refcount With refcount Net refcount cost
cMesh x64 32 40 +8 (2-byte count + 6 new padding bytes)
cMesh x86 20 24 +4 (2-byte count + 2 new padding bytes)
cEffect x64 48 56 +8 (2-byte count + 6 new padding bytes)
cEffect x86 16 16 0 (completely free)

Conclusion: only cEffect on x86/GL has its 2-byte reference count fit exactly into the 3-byte gap after m_bits (1 byte) and before the 4-byte alignment of m_programId — a true “zero cost”. In the other three cases, the two unsigned ints (or two shader pointers) are already individually aligned and the following pointer had no padding to begin with, so inserting the reference count pushes the pointer back and newly creates 6 (or 2) bytes of padding.

7.4 Can it be smaller / larger?

cMesh

  • Smaller: the size is determined by “two unsigned int counts + three platform handles”, not by the reference count. Making m_vertexCount/m_indexCount uint16_t would shrink x64 to 32 bytes and x86 to 20 bytes — but that caps each mesh at 65535 vertices/indices, which doesn’t hold for general geometry, so keeping unsigned int is the right trade-off. Keeping the two unsigned ints, the 2-byte reference count costs 40 bytes on x64 (8+2=10 must round to 16 to align the 8-byte pointers) and 24 bytes on x86 no matter where it’s placed.
  • Larger: always possible (make the count uint64_t, cache the raw sMeshData, add a debug name, etc.), but unnecessary.

cEffect

  • Smaller: of x64’s 56 bytes, cRenderState (32 bytes = 3 D3D state pointers) is the bulk, and those 3 pointers are required by the render-state object and can’t be removed. The reference count’s 6 padding bytes can’t be eliminated by reordering members (there are no other small fields to merge with), so 56 bytes is nearly minimal. x86’s 16 bytes is already the optimal “reference count completely free” result.
  • Larger: equally possible, just add fields.

7.5 Frame-render data memory budget (and whether pointer targets count)

Graphics.cpp caches one frame’s data in sDataRequiredToRenderAFrame and double buffers two copies (s_dataRequiredToRenderAFrame[2]):

Member x64 x86
constantData_frame (sFrame: two 4×4 matrices + 4 floats) 144 144
clearColor (4 floats) 16 16
meshEffectPairCount (uint16_t + padding) 8 4
meshEffectPairs[100] (each pair = mesh pointer + effect pointer) 100×16 = 1600 100×8 = 800
Single copy (compiler-measured) 1768 964
Double-buffered total (×2) 3536 bytes 1928 bytes

“Should the memory the pointers point to be counted in this budget?” — Conclusion: no. This budget measures the data Graphics caches to render one frame — i.e. the sDataRequiredToRenderAFrame struct itself (×2). The cMesh* / cEffect* in the array are just pointers; the vertex/index buffers, shaders, and render-state objects they point to are assets MyGame creates at initialization and holds long-term, so they aren’t part of the “frame cache”. The frame cache only records “which (mesh, effect) pair to draw this frame” and does not copy those assets. The frame-render memory budget is therefore reported as 3536 (x64) / 1928 (x86) bytes, not counting the pointer targets. The mesh/effect pair array is by far the bulk (1600 / 800 bytes), and the limit s_maxMeshEffectPairCount = 100 is fixed at compile time, with zero runtime dynamic allocation.

8. Why Frame Data Must Be Cached

Submission (SubmitMeshWithEffect) happens on the application loop thread, while the actual drawing (RenderFrame) happens on the main/render thread — the two run concurrently. Therefore:

  1. Decoupled timing: the application thread fills in what to draw this frame, and the render thread consumes it later. The data must live in a stable location between the two operations; you can’t count on drawing happening the moment Submit* is called.
  2. Double buffering avoids races: if one thread writes while the other reads the same data, you get a data race. So two copies of frame data ping-pong (s_dataBeingSubmittedByApplicationThread ↔ s_dataBeingRenderedByRenderThread) with two cEvents for synchronization: the application thread signals s_whenAllDataHasBeenSubmittedFromApplicationThread after filling; the render thread waits for that signal, swaps the two pointers, then signals s_whenDataForANewFrameCanBeSubmittedFromApplicationThread so the application thread can fill the next frame.
  3. Lifetime safety: meshes/effects belong to MyGame; Graphics only uses them temporarily. On submission, IncrementReferenceCount() gives Graphics a temporary reference so the object can’t be freed before rendering finishes; after rendering (or CleanUp), DecrementReferenceCount() returns it.
  4. Fixed budget, zero dynamic allocation: the cache uses fixed-size arrays, the memory ceiling is fixed at compile time, and there are no new/delete calls during gameplay — avoiding fragmentation and nondeterministic latency.

9. Build Verification

  • MSBuild YangFeiyang.sln /p:Configuration=Debug|Release /p:Platform=x64 (Direct3D) and /p:Platform=x86 (OpenGL) both compile cleanly (the sln-level platform name is x86, mapping to project-level Win32);
  • MyGame and ExampleGame both ran successfully in x64 Release and x86 Release;
  • x64 Release MyGame ran successfully and produced/verified the three screenshots above;
  • ExampleGame is left unchanged: it runs and shows solid black with no triangles;
  • the sizeof values are compiler-measured (see §7; the temporary probe code was removed after measuring).