SoftGPU: Wake only threads with tiles to draw, and chain the wakes

Waking a worker is a condition variable signal, a system call, and the main thread paid one per idle
worker every 48 queued items. With Tekken 6's tiny draws (about 5 vertices each) the workers keep
running dry, and those signals were about 10% of the main thread, which is what limits it there.

Now no more threads are woken than tiles have work, and only the first is woken from the main thread;
it wakes the rest.

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-05 14:55:04 -06:00
1 parent bec1eacd77
commit 1228837688
2 files changed
+40 -6

No files matched your search

+37 -6
View File
@@ -20,6 +20,7 @@
#include <condition_variable>
#include <thread>
#include "Common/BitSet.h"
#include "Common/Profiler/Profiler.h"
#include "Common/Thread/ParallelLoop.h"
#include "Common/Thread/ThreadManager.h"
@@ -126,6 +127,7 @@ public:
}
void Run() override {
binner_->WakeChained();
binner_->ProcessTiles(index_);
status_ = false;
// Work queued after the last look, but before status_ said we were done, would otherwise wait.
@@ -652,18 +654,47 @@ void BinManager::ReclaimItems() {
}
void BinManager::WakeTasks() {
int threads = 0;
for (int i = 0; i < maxTasks_; ++i) {
threads++;
if (taskStatus_[i])
// No more threads than tiles with work.
int pending = 0;
const int count = activeCount_.load(std::memory_order_relaxed);
for (int n = 0; n < count && pending < maxTasks_; ++n) {
const Tile &tile = tiles_[activeTiles_[n]];
if (tile.head.load(std::memory_order_relaxed) != tile.tail.load(std::memory_order_relaxed))
pending++;
}
int first = -1;
uint64_t chain = 0;
for (int i = 0; i < maxTasks_ && pending > 0; ++i) {
if (taskStatus_[i]) {
pending--;
continue;
}
waitable_->Fill();
taskStatus_[i] = true;
g_threadManager.EnqueueTaskOnThread(i, taskLists_[i].Next());
if (first < 0)
first = i;
else
chain |= 1ULL << i;
pending--;
enqueues_++;
}
mostThreads_ = std::max(mostThreads_, threads);
if (first < 0)
return;
if (chain != 0)
chainWake_.fetch_or(chain, std::memory_order_release);
g_threadManager.EnqueueTaskOnThread(first, taskLists_[first].Next());
mostThreads_ = std::max(mostThreads_, maxTasks_);
}
void BinManager::WakeChained() {
uint64_t chain = chainWake_.exchange(0, std::memory_order_acquire);
while (chain != 0) {
const int i = LeastSignificantSetBit(chain);
chain &= chain - 1;
g_threadManager.EnqueueTaskOnThread(i, taskLists_[i].Next());
}
}
// The queue is full: with threads, wait for drawn items to free up, helping with the drawing.
+3
View File
@@ -264,6 +264,8 @@ private:
int maxTasks_ = 1;
BinTaskList taskLists_[MAX_POSSIBLE_TASKS];
std::atomic<bool> taskStatus_[MAX_POSSIBLE_TASKS];
// Threads whose tasks the first one woken enqueues: waking a thread is a system call, kept off this one.
std::atomic<uint64_t> chainWake_{ 0 };
// With threads, queued items are binned into screen tiles. Any thread can take a tile with work and
// draws its items in order; only one at a time, so each pixel still sees the primitives in order.
@@ -359,6 +361,7 @@ private:
bool NeedsOrder(const BinItem &item);
void ReclaimItems();
void WakeTasks();
void WakeChained();
bool ProcessTiles(int start);
friend class DrawBinItemsTask;