SoftGPU: Keep a tile's tail off the drawing threads' cache line

A tile's head and busy flag, which the drawing threads write, shared a
cache line with its tail, which the emulation thread writes for every
item it hands the tile. And a drawing thread waiting for work read
every tile's head and tail again and again. Now the tail has a line of
its own, and a waiting thread watches one counter that DistributeItems
bumps, looking at the tiles only when it changes.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Henrik RydgårdandClaude Opus 5.5 committed 2026-10-10 15:01:49 +02:00
1 parent 061038e0bb
commit ce83a7d366
2 files changed
+20 -7

No files matched your search

+14 -4
View File
@@ -673,6 +673,7 @@ void BinManager::DistributeItems() {
void BinManager::DistributeItems(size_t end) {
undistributed_ = 0;
bool any = false;
while (distributePos_ != end) {
const size_t index = distributePos_;
const BinItem &item = queue_[index];
@@ -701,7 +702,10 @@ void BinManager::DistributeItems(size_t end) {
}
}
distributePos_ = index + 1 == QUEUED_PRIMS ? 0 : index + 1;
any = true;
}
if (any)
distributed_.store(distributed_.load(std::memory_order_relaxed) + 1, std::memory_order_release);
}
void BinManager::ResetTiles() {
@@ -784,17 +788,23 @@ void BinManager::MakeRoom() {
// A drawing thread out of work stays a moment for more before its task ends: waking it again costs the
// emulation thread a system call (about 15 us under WSL2, where lingering made God of War and Wipeout 4-8%
// faster). Not while that thread waits for the drawing to finish.
// It looks at the tiles again only when distributed_ says there's new work: reading their heads and tails over
// and over took cache lines from the emulation thread, which writes the tails.
void BinManager::LingerForWork(int start) {
double until = time_now_d() + LINGER_SECONDS;
while (!waitingForIdle_.load(std::memory_order_relaxed)) {
// Read before the look at the tiles, so work distributed during it isn't missed.
const uint32_t seen = distributed_.load(std::memory_order_acquire);
if (ProcessTiles(start)) {
until = time_now_d() + LINGER_SECONDS;
continue;
}
if (time_now_d() >= until)
break;
for (int i = 0; i < 64; ++i)
std::this_thread::yield();
while (distributed_.load(std::memory_order_acquire) == seen && !waitingForIdle_.load(std::memory_order_relaxed)) {
if (time_now_d() >= until)
return;
for (int i = 0; i < 64; ++i)
std::this_thread::yield();
}
}
}
+6 -3
View File
@@ -313,10 +313,11 @@ private:
int tilesX_ = TILES_X;
int tilesY_ = TILES_Y;
struct Tile {
// Indices into queue_, as a ring: head_ is how many have been drawn, tail_ how many were pushed.
std::atomic<uint32_t> head;
std::atomic<uint32_t> tail;
// Indices into queue_, as a ring: head_ is how many have been drawn, tail_ how many were pushed. The
// drawing threads write head and busy, this thread tail, so they're on separate cache lines.
alignas(64) std::atomic<uint32_t> head;
std::atomic<bool> busy;
alignas(64) std::atomic<uint32_t> tail;
uint16_t items[QUEUED_PRIMS];
};
Tile *tiles_ = nullptr;
@@ -325,6 +326,8 @@ private:
// The tiles given work since the last flush, for the threads to look through.
uint16_t activeTiles_[TILES_X * TILES_Y];
std::atomic<int> activeCount_{ 0 };
// Counts DistributeItems calls that gave the tiles work, for threads waiting for some (LingerForWork).
std::atomic<uint32_t> distributed_{ 0 };
// The emulation thread is waiting for the drawing threads to finish (LingerForWork).
std::atomic<bool> waitingForIdle_{ false };
bool tileActive_[TILES_X * TILES_Y]{};