Step 38 · First contribution · D4

Reproducible debugging labs

Three native C++ exercises with deliberate failures, reference fixes, boundary tests and explicit limits.

Before this page: Bytes, bits and bounded reads · Objects, owners and borrowed memory · Submitted is not completed · Reading a run. View the learning route.

Read, trace, prove
What these labs do and do not test

Three deterministic C++ exercises introduce mistakes in fixture callers, not in the emulator checkout. Alignment and lifetime use the real src/common/alignment.h and src/common/lruCache.h. The output-contract API is explicitly fictional. No game, Vulkan device, Qt, console firmware or third-party emulator build is needed. Passing them is not proof of game compatibility.

Read the developer gates first. The fixture source is developer_labs.cpp; build definitions are in CMakeLists.txt. The lab README provides a compact offline reference.

Build the isolated harness

From the learning repository root, use CMake 3.20+, Ninja and a C++20 compiler. On Windows the following was checked with Clang 22.1.3 and installed MSVC/Windows SDK libraries; if discovery fails, use an x64 Visual Studio developer shell. On Linux/macOS select your installed C++20 compiler. This is not the full emulator's toolchain recipe.

cmake -S docs/labs -B _Build/curriculum -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER=clang++
cmake --build _Build/curriculum
ctest --test-dir _Build/curriculum --output-on-failure

The default emulator source location is KytyPS5/ beside docs/. For another checkout, add -DKYTY_SOURCE_DIR="D:/path/to/KytyPS5" to configuration. Start with an unused build directory if changing compilers; do not reuse an incompatible CMake cache. The lab build does not configure, edit or build the whole emulator.

Expected verification result: 7/7 CTest cases pass. Three run reference solutions, three confirm the exact intentional failures, and one compiles/runs the existing tests/LruCacheTests.cpp. The failure checker requires exit code 1 and the exact expected diagnostic; a crash, missing executable or unrelated assertion is not counted as success.

The commands below use the Windows Ninja executable path. On Linux/macOS omit .exe. Each buggy invocation is expected to print FAIL and exit 1; each fixed invocation prints PASS and exits 0. Invalid CLI arguments exit 2.

Lab A: the range crosses a page even when the size does not

Contract: for a nonempty byte range [address,address+size), return the smallest enclosing 16 KiB page interval. Empty input returns the original empty interval; any overflow in the endpoint or rounded endpoint is rejected. This is a fixture contract, not the behavior of every emulator allocation API.

_Build/curriculum/developer_labs.exe alignment buggy
FAIL [alignment] cross-page: expected [0,32768), got [0,16384)

_Build/curriculum/developer_labs.exe alignment fixed
PASS [alignment] 7 cases
Input / operationHexDecimal
Page size0x400016,384
Address / size0x3FFC / 0x816,380 / 8
Exclusive byte endpoint0x400416,388
Correct page interval[0x0000,0x8000)[0, 32,768)
Buggy interval[0x0000,0x4000)[0, 16,384)

Open Cover, stop at EXERCISE A and inspect address, size, begin and end. Predict the result for size 4 before running it: an exclusive endpoint exactly on a boundary should not add another page. Explain why this helper cannot safely rely on unsigned wraparound.

Reference fix and why it works

Round the absolute exclusive endpoint, not the byte count: Range{begin, Common::AlignUp(end, PageSize)}. The wrapper first checks addition and rounding overflow. Common::AlignUp itself is an arithmetic helper; it does not promise overflow rejection. Tests cover crossing, exact end, aligned input, one byte, empty input, addition overflow and alignment overflow.

Transfer task: trace one real range consumer in the memory subsystem. Write its actual alignment and ownership preconditions before proposing a change. A page-cover result alone does not validate that a guest mapping exists or is readable.

Lab B: last-use tick is not completed tick

Contract: enumerate retirement candidates whose last recorded use is no later than observed completion. Insert A at tick 2 and B at tick 3; A is reused in submission 6 while completion is still 4. The helper's cutoff is inclusive: equal ticks are eligible. The example supplies chronologically ordered insertions/touches; it does not prove arbitrary out-of-order tick insertion is safe.

_Build/curriculum/developer_labs.exe lifetime buggy
FAIL [lifetime] cutoff-4: expected B, got B,A

_Build/curriculum/developer_labs.exe lifetime fixed
PASS [lifetime] 5 observations

At EXERCISE B, compare completed_tick with last_use_tick. Draw the list order after the touch. The cache knows only the tick you give it: it cannot detect that the caller recorded the wrong one.

Reference fix and why it works

Call cache.Touch(a, last_use_tick), using 6 rather than 4. Cutoff 4 returns only B. An older touch at 5 and an equal touch at 6 must not move A's last-use point backwards; cutoff 5 still returns B, and cutoff 6 returns B,A. An empty cutoff and explicit removal are also checked.

ForEachItemBelow enumerates; it does not automatically free. The harness explicitly calls Free only after testing completion eligibility. The real upstream LRU tests additionally exercise early-exit callbacks and ID reuse.

Transfer task: compare this invariant with CommandScheduler::DeferOperation and PopPendingOperations. Explain why the generic cache exercise says nothing by itself about queue ownership, barriers, image layouts, later submissions or priority callbacks. Use D3's completion trace.

Lab C: returning an error can still corrupt the caller

Fictional contract: QueryStatus receives trusted local C++ objects. Request size must be 8, flags must be zero, and neither pointer may be null. Success returns 0 and writes state 3/capacity 16. Invalid input returns −1 and leaves any supplied output unchanged. These numbers, names and layouts are not a Sony API, a real NID or an implementation suggestion.

_Build/curriculum/developer_labs.exe contract buggy
FAIL [contract] invalid-size: output changed on error

_Build/curriculum/developer_labs.exe contract fixed
PASS [contract] 5 cases

The buggy path clears output before validating the size/flags. Its error code is correct, its trace would show that it ran, and the success case passes—yet the caller's sentinel values are destroyed on error. At EXERCISE C, inspect output before and after an invalid request.

Reference fix and why it works

Remove the pre-validation *output = {}; write the final status only after all request checks pass. The tests cover success, invalid size, unsupported flags, null request and null output. They check both return values and output bytes represented by the two fields.

Do not generalize “output unchanged on error” to every real API. Establish that API's actual contract first. This harness intentionally excludes arbitrary invalid non-null pointers, guest memory probing, overlap, concurrency and guest-to-host ABI behavior.

Transfer task: choose an existing small HLE function and write an input/output/error table from evidence. Identify size, offset, signedness and ownership rules. If the contract is unknown, mark the uncertainty; changing constants until a title gets further is not a conformance test.

Your red → green workflow

  1. Run the untouched buggy mode. Save its diagnostic and exit code ($LASTEXITCODE in PowerShell).
  2. Write a prediction and inspect the marked caller with a debugger or source-level reasoning. Do not edit the upstream headers to accommodate a broken caller.
  3. Make your local fix in the buggy branch, rebuild, and run the same buggy command again. It should now pass the existing assertions. Keep tests intact.
  4. Compare with fixed mode only after explaining your own correction. Add an additional boundary or negative case and explain what alternative wrong fix it would catch.
  5. Record whether your evidence is from fixture tests, actual upstream tests or a live emulator run.

The checked-in CTest suite verifies the teaching baseline, including its intentionally broken variants. After you solve a buggy branch, its *_expected_failure case will correctly complain that the expected failure disappeared. While experimenting, run the now-fixed branch directly and ctest --test-dir _Build/curriculum -R "fixed|upstream_lru" --output-on-failure. If keeping your solved version, replace that expected-failure test with a normal success test. Do not restore the bug merely to make a baseline meta-test green.

Extend into actual emulator regression tests

The isolated build already runs upstream LruCacheTests.cpp. The following are advanced source-backed follow-ups, not additional game-free fixture tests and not claimed as executed by this lesson. They use the full emulator test build and its dependencies. First inspect the target definitions in the checked-out CMakeLists.txt.

AreaExisting entry pointAssignment
Sparse backingtests/VirtualMemoryAllocationTests.cpp: TestSparseBackingReadPreservesResidency, TestSparseReadDuringDirectCommit.Explain owned holes versus unowned/private mappings; identify which assertions establish byte contents and which establish unchanged residency. Design one additional boundary case without changing intended sparse semantics.
APR/filestests/KernelFileSystemTests.cpp: CheckAprPaths.Trace the registered API to the command queue. Identify the append-order and address-wait behavior actually checked, then document a missing case instead of assuming the test covers every priority interaction.

From the emulator root, with an already configured compatible build directory and BUILD_TESTING=ON, these are the target/CTest names at the pinned revision. Replace the example build directory with yours:

cmake --build _Build/windows --target virtual_memory_allocation_tests kernel_file_system_tests
ctest --test-dir _Build/windows -R "^(virtual_memory_allocation|kernel_file_system)$" --output-on-failure

These executables are not GoogleTest binaries; do not invent --gtest_filter support to select the named internal functions. The CTest expression selects complete registered tests. Check build and test exit codes separately and report missing prerequisites, not a fabricated passing run.

Pass the debugging gate

Checkpoint

Finish with the capstone evidence record, not just a screenshot of green checkmarks.