Step 05 · Machine essentials · B5

Submitted is not completed

Keep an object alive until the work using it has actually finished.

Before this page: Instructions, addresses and the guest. View the learning route.

Learn, run, explain

Before you start

Complete B4, and recall B3's distinction between an owner and a borrow. This program models lifetime bookkeeping in one thread; it does not submit Vulkan work or implement synchronization.

Two clocks of progress

The CPU can enqueue work and continue while a device is still processing earlier work. Submitted means the request was handed off; completed means the relevant work has finished. A resource used by an unfinished request must remain valid. Imagine tickets 7 and 9 on one ordered queue: seeing completion 7 does not let you release something also used by ticket 9.

Our resource records an id and its last-use ticket. A completed value permits retirement only when last_use <= completed. A real implementation must associate the value with the correct queue/timeline and actual completion signal. Writing a number in a CPU variable does not wait for a GPU.

Read a small callback

std::erase_if removes vector elements for which a predicate returns true. The expression [completed](const Resource& resource) { ... } is a lambda: a small unnamed function object. It captures a copy of completed, borrows one resource for inspection, and returns whether that resource may be removed. This is your first callback; templates and forwarding follow in project C++ by example, after the codebase map.

#include <algorithm>
#include <cstdint>
#include <iostream>
#include <vector>

// Single-threaded bookkeeping model: no GPU work or synchronization is performed.
struct Resource { int id; std::uint64_t last_use; };

void retire(std::vector<Resource>& retained, std::uint64_t completed) {
    std::erase_if(retained, [completed](const Resource& resource) {
        return resource.last_use <= completed;
    });
}

int main() {
    std::vector<Resource> retained{{1, 7}};
    const std::uint64_t submitted = 7;
    std::uint64_t completed = 6;
    retire(retained, completed);
    if (retained.size() != 1) return 1;
    std::cout << "submitted=" << submitted << " completed=" << completed
              << " retained=" << retained.size() << '\n';
    // A later submission reuses resource 1 before submission 7 completes.
    retained[0].last_use = 9;
    completed = 7;
    retire(retained, completed);
    if (retained.size() != 1) return 1;
    std::cout << "completed=7 retained=" << retained.size() << '\n';
    completed = 9;
    retire(retained, completed);
    if (!retained.empty()) return 1;
    std::cout << "completed=9 retained=" << retained.size() << '\n';
    retire(retained, completed); // Retiring an empty list is valid.
    if (!retained.empty()) return 1;
}

Use the B1 setup once. From this learning repository's root:

cmake --build _Build/curriculum/beginner --target completion
ctest --test-dir _Build/curriculum/beginner -R '^beginner_completion$' --output-on-failure

To see the output directly in PowerShell, run .\_Build\curriculum\beginner\completion.exe; in a POSIX shell, run ./_Build/curriculum/beginner/completion. Tests compare the full output and require a successful exit.

Expected output, with exit status 0:

submitted=7 completed=6 retained=1
completed=7 retained=1
completed=9 retained=0

Where threads and locks enter

A thread is an independently scheduled sequence of CPU execution. If one CPU thread updates a shared container while another reads it, they need an appropriate synchronization protocol. A mutex grants one thread at a time access to a protected critical section; an RAII lock releases it when its scope ends. A C++ data race on ordinary shared data can cause undefined behavior.

That CPU lock protects access to the bookkeeping. It does not by itself establish that device work is complete. Conversely, a completion value does not automatically make unsynchronized CPU access to the container safe. Read C4 before changing threaded project code.

Exercise

Draw the resource's last-use value above each completion value. Change the last-use update from 9 to 10: should completion 9 free it? Update the fixture to observe completion 10 before expecting an empty list. Then consider two resources, last used at 7 and 10, and predict which remain at completion 9.

Hint

Retirement compares each resource's last use, not the first submission that ever mentioned it.

Worked answer and rubric

With last use 10, completion 9 retains the resource; completion 10 retires it. With resources used at 7 and 10, only the latter remains at completion 9. Update both the program's check and output file to reflect your intended additional observation. The baseline tests cover incomplete work, reuse by a later submission, equality at completion and an empty list. You are ready when you can explain why using submitted in place of completed would be an early-release bug.

Follow real ownership through completion

Compare this model with src/graphics/host_gpu/renderer/commandScheduler.cpp and src/graphics/host_gpu/renderer/masterSemaphore.cpp after the graphics introduction. D4's lifetime lab gives you a deliberate bookkeeping bug to fix; D3 connects the model to an actual draw path.

Before moving on

Ticks record your own assessment. Keep your prediction, actual output and explanation; a tick alone is not evidence of understanding.