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 runMyGame.exe.
Controls: holdHto hide the rectangle; holdAto switch the triangle to the animated effect; pressEscto close the window.
1. Overview
On top of Assignment 03, this assignment completes the following core work:
- Reference-counted resources:
cMeshandcEffectbecome reference-counted objects using the macros inReferenceCountedAssets.h, and provide staticLoad()factory functions. - Submission interfaces: the Graphics project gains two interfaces —
SubmitMeshWithEffect(cMesh&, cEffect&)andSubmitClearColor(sColor)— for the application to call on the submit thread. - Frame data caching: submitted mesh/effect pairs are cached in a fixed-size array with zero runtime dynamic allocation.
- 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.
- Keyboard interaction: hold
Hto hide the rectangle; holdAto make the triangle use the animated effect. - 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:
|
Key macro behavior (ReferenceCountedAssets.h):
IncrementReferenceCount()/DecrementReferenceCount()useInterlockedIncrement16/InterlockedDecrement16(thread-safe atomic operations); when the count reaches0,delete thisis executed — i.e. the last referencer is responsible for freeing.- The reference count
m_referenceCountis auint16_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
|
4.2 Submit interfaces
|
4.3 Rendering and cleanup
|
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)
|
5.2 Key handling
|
5.3 Submit (every frame)
|
5.4 Clean up
|
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) |



All three states were verified pixel-by-pixel to match expectations:
- Default: both meshes are drawn with the standard (white) effect on a non-black blue background — proving
SubmitClearColorandSubmitMeshWithEffectwork. - Holding H: the rectangle area becomes the blue background — proving the rectangle is no longer submitted.
- 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 intcounts + three platform handles”, not by the reference count. Makingm_vertexCount/m_indexCountuint16_twould 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 keepingunsigned intis the right trade-off. Keeping the twounsigned 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 rawsMeshData, 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:
- 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. - 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 twocEvents for synchronization: the application thread signalss_whenAllDataHasBeenSubmittedFromApplicationThreadafter filling; the render thread waits for that signal, swaps the two pointers, then signalss_whenDataForANewFrameCanBeSubmittedFromApplicationThreadso the application thread can fill the next frame. - 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 (orCleanUp),DecrementReferenceCount()returns it. - Fixed budget, zero dynamic allocation: the cache uses fixed-size arrays, the memory ceiling is fixed at compile time, and there are no
new/deletecalls 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 isx86, mapping to project-levelWin32);MyGameandExampleGameboth ran successfully in x64 Release and x86 Release;- x64 Release
MyGameran successfully and produced/verified the three screenshots above; ExampleGameis left unchanged: it runs and shows solid black with no triangles;- the
sizeofvalues are compiler-measured (see §7; the temporary probe code was removed after measuring).