mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
252 lines
15 KiB
Markdown
252 lines
15 KiB
Markdown
# sceGe: how the PSP's display list queue behaves
|
|
|
|
This describes the behavior `GPU/GPUCommon.cpp` (the queue) and `Core/HLE/sceGe.cpp` (the
|
|
interrupt side) model. All of it is what a game can observe, and all of it is pinned down on a
|
|
real PSP by the tests in `pspautotests/tests/gpu/ge` (`queue`, `queue2`, `break`, `breakwait`,
|
|
`callbackstate`, `intrsuspend`, `enqueueparam`) and `pspautotests/tests/gpu/signals`.
|
|
|
|
The headline, because everything else follows from it: **the GE stops at every SIGNAL and every
|
|
FINISH, and it takes the interrupt to get it going again.** After a signal it carries on with the
|
|
same list. After a FINISH the game's finish callback runs *first*, and only then does the list
|
|
leave the queue and the next one start. Nothing the game calls in between makes the GE run.
|
|
|
|
## Lists and their states
|
|
|
|
Up to 64 lists can exist at once; with all of them in use, enqueueing fails with `0x80000022`.
|
|
Ids are opaque, and an id is reused once its list is gone.
|
|
|
|
| State | Meaning |
|
|
|---|---|
|
|
| NONE | Not a list. `sceGeListSync(id, 1)` gives `0x80000100`. |
|
|
| QUEUED | On the queue, not executing. |
|
|
| RUNNING | The GE is executing it, or is stalled on it. |
|
|
| COMPLETED | Hit its FINISH and is off the queue. The id can still be asked about, until `sceGeDrawSync(0)` turns every COMPLETED list into NONE. |
|
|
| PAUSED | Stopped by `sceGeBreak(0)`, by a PAUSE signal, or put at the head with `sceGeListEnQueueHead`. |
|
|
|
|
Besides its state, a list remembers whether it **has started executing** at some point
|
|
(`DisplayList::started`), which several checks below depend on, and whether something is pending
|
|
on it: a pause that was requested but hasn't been delivered, a pause that has been, or a SYNC.
|
|
`DisplayList::signal` holds that, in the same enum as the behaviors of the SIGNAL command, with
|
|
`PSP_GE_SIGNAL_HANDLER_SUSPEND` doubling as "paused, and the game has been told".
|
|
|
|
## Enqueueing
|
|
|
|
`sceGeListEnQueue` and `sceGeListEnQueueHead` differ only in where the list goes. In order:
|
|
|
|
1. The list, stall, context and stack addresses all have to be 4-byte aligned (`0x80000103`), and
|
|
a stack depth of 256 or more is `0x80000104`.
|
|
2. Games built with SDK version `0x01FFFFFF` or below get a 32-entry stack they didn't ask for,
|
|
and the next step can't fail for them. Otherwise the stack is whatever `PspGeListArgs` says,
|
|
which may well be none.
|
|
3. **The new list is compared against every list on the queue:**
|
|
- Same list address: `0x80000021`. What's compared is the address the other list was
|
|
*enqueued* with, not where it has got to since, and the uncached mirror of an address
|
|
counts as the same. After `sceGeBreak(0)` stops a list, it's the address it stopped at that
|
|
collides instead, and the address it was enqueued with is free again.
|
|
- Same stack, **and the other list has started**: `0x80000021`. Two lists that are both still
|
|
waiting their turn can share a stack without complaint.
|
|
4. Then, depending:
|
|
- *Tail, queue empty:* RUNNING, and the GE starts on it right away. If it has a context, the
|
|
GE state is saved into it first.
|
|
- *Tail, queue not empty:* QUEUED at the back.
|
|
- *Head, queue not empty:* the current head has to be PAUSED, or it's `0x800001FE`. The new list
|
|
becomes PAUSED at the head, and the old head QUEUED behind it.
|
|
- *Head, queue empty:* PAUSED, and **not started**. It takes a `sceGeContinue` to run it.
|
|
|
|
"Every list on the queue" includes a list whose finish callback is running right now. So a
|
|
finish callback that enqueues its own list again gets `0x80000021` (on a newer SDK), and anything
|
|
else it enqueues is QUEUED behind it rather than started.
|
|
|
|
## SIGNAL
|
|
|
|
A SIGNAL followed by END stops the GE and raises an interrupt. It always applies to the list at
|
|
the head of the queue.
|
|
|
|
| Behavior | What happens |
|
|
|---|---|
|
|
| `HANDLER_SUSPEND` (1) | The signal callback runs **with the GE stopped**, then the GE carries on. For SDK <= `0x02000010`, the list reads as PAUSED for the duration. |
|
|
| `HANDLER_CONTINUE` (2) | The GE carries on, *then* the signal callback runs. |
|
|
| `HANDLER_PAUSE` (3) | The list becomes PAUSED **immediately**, the argument is remembered, and the GE carries on. No callback yet. See below. |
|
|
| `SYNC` (8) | The GE carries on, and the list's next FINISH is swallowed: no callback, no completion, it just keeps going. |
|
|
| `JUMP`, `CALL`, `RET`, and their relative and origin variants (0x10 - 0x16) | Done in software by the interrupt. CALL uses the list's stack; overflowing or underflowing it leaves the GE stopped. |
|
|
| 0x20 - 0x2F, 0x30, 0x38 | Set a texture or CLUT address relative to the list or to the offset address. PPSSPP doesn't implement these. |
|
|
|
|
PPSSPP treats `HANDLER_CONTINUE` like `HANDLER_SUSPEND`: the GE waits for the callback. On
|
|
hardware the two run side by side, so a callback that looks at the GE finds it further along,
|
|
which is all `gpu/signals/continue` fails on. Waiting is the safe side to err on - a callback
|
|
that prepares something the list is about to use still gets there first.
|
|
|
|
Callbacks get the low 16 bits of the SIGNAL (or FINISH) command, the argument they were
|
|
registered with, and - for SDK > `0x02000010` only, otherwise 0 - the address after the END.
|
|
|
|
### The pause window
|
|
|
|
Between a PAUSE signal and the FINISH that delivers it, a list is in an odd place. The GE is
|
|
still executing it, but:
|
|
|
|
- `sceGeListSync(id, 1)` already says PAUSED.
|
|
- `sceGeContinue` returns `0x80000021`. So does `sceGeBreak(0)`.
|
|
- `sceGeListUpdateStallAddr` returns 0 but **has no effect on the GE**, because only a RUNNING
|
|
list's stall address does. So a list that stalls inside the window is stuck for good, and
|
|
`sceGeBreak(1)` is the only way out. Games therefore never put a stall there, and neither
|
|
should a test that wants to finish.
|
|
|
|
When the FINISH arrives, the **signal** callback (not the finish one) is called, with the
|
|
argument from the PAUSE signal. The list stays PAUSED at the head of the queue, and nothing
|
|
behind it runs until the game calls `sceGeContinue`, which now works.
|
|
|
|
## FINISH
|
|
|
|
A FINISH followed by END stops the GE and raises an interrupt. If a SYNC or a pause is pending on
|
|
the list, see above. Otherwise, in this order:
|
|
|
|
1. The list becomes COMPLETED. It is still at the head of the queue.
|
|
2. The **finish callback** runs. The GE is idle, and its state is still what the list left it as:
|
|
a context isn't restored until the next step. From in here, `sceGeListSync` on the next list
|
|
says QUEUED, `sceGeDrawSync(1)` says DRAWING if there is a next list and COMPLETED if not,
|
|
`sceGeListDeQueue` on the finished list says `0x80000021`, and `sceGeListUpdateStallAddr` on
|
|
it says `0x80000020`.
|
|
3. If the list had a context, the GE state is restored from it.
|
|
4. The list leaves the queue.
|
|
5. The new head, if there is one and it isn't PAUSED, is started. If it has a context and hasn't
|
|
started before, the GE state is saved into it.
|
|
6. If the queue is empty instead, threads waiting in `sceGeDrawSync(0)` are woken.
|
|
7. Finally the threads waiting in `sceGeListSync(id, 0)` for the finished list are woken - after
|
|
the ones from step 6, which shows when they have the same priority.
|
|
|
|
If a SIGNAL and a FINISH are both pending by the time the interrupt runs, only the FINISH counts.
|
|
|
|
### Is the GE busy?
|
|
|
|
`sceGeSaveContext` and `sceGeRestoreContext` fail with -1 while the GE is *executing*, and that's
|
|
all they go by - not whether lists are queued. A GE waiting at a stall address counts as
|
|
executing. One that has stopped at a SIGNAL or FINISH doesn't, so both work from a finish
|
|
callback and from a `HANDLER_SUSPEND` callback, however much is queued behind. From a
|
|
`HANDLER_CONTINUE` callback they fail, as the GE is off again by then. `GPUCommon::BusyDrawing()`.
|
|
|
|
### While the interrupt can't be taken
|
|
|
|
All of the above happens in the interrupt, so with interrupts off (`sceKernelCpuSuspendIntr`)
|
|
the queue simply stands still at the first FINISH: that list keeps reading as DRAWING, the next
|
|
one as QUEUED, and no callback runs. It all catches up, in order, when interrupts come back.
|
|
Suspending thread dispatch (`sceKernelSuspendDispatchThread`) doesn't hold any of it up.
|
|
|
|
### How PPSSPP does this
|
|
|
|
PPSSPP doesn't run the GE in parallel with the CPU. `ProcessDLQueue()` executes a list all the way
|
|
to whatever stops it - a stall, a SIGNAL, a FINISH - adding up how long that would have taken, and
|
|
schedules the interrupt for that point in the future. That's the accepted inaccuracy, and it means
|
|
there's a stretch where a list has reached its FINISH but the game hasn't been told, which doesn't
|
|
exist on hardware. `DisplayList::pendingInterrupt` marks it.
|
|
|
|
What keeps the *sequencing* right in spite of that is one rule in `ProcessDLQueue()`: **while the
|
|
head of the queue has an interrupt pending, nothing runs**, whoever asks - an enqueue, a stall
|
|
update or a continue. The finished list stays on the queue until `InterruptEnd()`, which takes it
|
|
off and lets the next one go, as in steps 4 to 7 above. Before that rule, the next list ran right
|
|
away and the finished one left the queue at once, so a finish callback saw an empty queue: a list
|
|
it enqueued was started instead of queued, and then couldn't be dequeued.
|
|
|
|
Three places deliberately treat a list with its FINISH pending as already gone, since on hardware it
|
|
would be: the duplicate check when enqueueing (Exit enqueues the same list again right after it
|
|
ends), `drawCompleteTicks`, which is set when the last list reaches its FINISH rather than
|
|
when the interrupt is delivered, so that a `sceGeDrawSync(0)` in between doesn't wait for nothing,
|
|
and `sceGeBreak(1)`. That one throws away interrupts that have been *raised* and couldn't be taken,
|
|
as hardware does, but not the ones we simply haven't raised yet: a game that breaks right after
|
|
its last list and then waits for what the finish callback signals had that callback long ago.
|
|
|
|
## Waiting
|
|
|
|
The two waiting calls, `sceGeListSync(id, 0)` and `sceGeDrawSync(0)`, refuse outright where a
|
|
thread can't wait, *even if there is nothing to wait for*: `0x80020064` from a GE callback or
|
|
any other interrupt, `0x800201A7` with dispatch or interrupts suspended.
|
|
|
|
`sceGeListSync(id, 0)` doesn't care what state the list is in. It waits for that id to complete or
|
|
be dequeued, and for an id that isn't pending - never used, or done - it returns 0 at once.
|
|
|
|
`sceGeListSync(id, 1)`:
|
|
|
|
| List state | Result |
|
|
|---|---|
|
|
| RUNNING | STALLING (3) if the GE is at the stall address, else DRAWING (2) |
|
|
| QUEUED | PAUSED (4) if it has started before (an EnQueueHead bumped it), else QUEUED (1) |
|
|
| COMPLETED | 0 |
|
|
| PAUSED | 4 |
|
|
| NONE | `0x80000100` |
|
|
|
|
`sceGeDrawSync(0)` waits for the queue to drain, and then **turns every COMPLETED list into
|
|
NONE**. Nothing else does, short of `sceGeBreak(1)`. `sceGeDrawSync(1)` looks at the head of the
|
|
queue, or the one after if the head is COMPLETED, and answers STALLING or DRAWING depending on
|
|
whether the GE is at that list's stall address - which it can't be if it isn't on that list.
|
|
|
|
## Break, continue, dequeue, stall
|
|
|
|
`sceGeBreak(0)`, by the state of the head of the queue:
|
|
|
|
| State | Result |
|
|
|---|---|
|
|
| RUNNING | The GE is stopped, and the list becomes PAUSED, remembering where it got to. If that was in the middle of a SIGNAL/END or FINISH/END pair, it resumes from the start of the pair. Returns the id. |
|
|
| QUEUED | Becomes PAUSED, id returned. |
|
|
| PAUSED | `0x80000021` for SDK <= `0x02000010` or inside the pause window, otherwise `0x80000020`. |
|
|
| no list | `0x80000020` |
|
|
|
|
**`sceGeBreak(1)` throws the whole queue away** and resets the GE: every list becomes NONE. An
|
|
interrupt that was raised but not taken yet goes too, so a list that reached its FINISH just
|
|
before never gets its finish callback. It doesn't wake anybody. A thread in `sceGeListSync(id, 0)` or `sceGeDrawSync(0)` stays there - until
|
|
a *new* list happens to get the same id and completes, or the queue next drains, at which point
|
|
they return 0 as if nothing had happened.
|
|
|
|
`sceGeContinue`: with no list, 0. A PAUSED head resumes where it left off (`0x80000021` inside the
|
|
pause window). On a RUNNING head, `0x80000020`; anything else, `0x80000004`. Both of those are -1
|
|
for SDK <= `0x01FFFFFF`.
|
|
|
|
`sceGeListDeQueue`: `0x80000100` for NONE, `0x80000021` for a list that has started - which
|
|
includes COMPLETED ones, until `sceGeDrawSync(0)` - and otherwise the list leaves the queue,
|
|
becomes NONE, and threads waiting for it are woken. Other lists are left alone: if that was the
|
|
last one queued, COMPLETED lists stay COMPLETED, and nobody in `sceGeDrawSync(0)` is woken.
|
|
|
|
`sceGeListUpdateStallAddr`: takes effect only if the list is RUNNING, and a GE stalled on that
|
|
list resumes by itself. For QUEUED and PAUSED lists it's just remembered for when they run.
|
|
COMPLETED is `0x80000020`.
|
|
|
|
One thing not to do, in a test or anywhere: `sceGeBreak(0)` from inside a `HANDLER_SUSPEND`
|
|
callback with an SDK version above `0x02000010`. With an older one the list is PAUSED for the
|
|
duration and the break is refused (`0x80000021`). With a newer one it goes through, the GE is
|
|
restarted anyway when the callback returns, and the PSP hangs.
|
|
|
|
## Starting another executable
|
|
|
|
`sceKernelLoadExec` restarts the kernel, and the GE driver with it: the new executable finds an
|
|
empty queue, whatever the old one left on it, and a GE with every register and matrix set to
|
|
zero, which is how the driver starts up - the same as a program that was booted directly.
|
|
`GPUCommon::Reinitialize()` is that, and has to leave nothing behind - not the lists, not the
|
|
queue of their ids, and not the old program's GE state. (This part isn't confirmed by a test:
|
|
whatever runs after the restart can't report back through PSPLink.) It used to keep the
|
|
queue, so the old executable's ids came back as lists with no state and a pc of 0, waiting their
|
|
turn behind the new executable's first list, where they'd block everything. Crazy Taxi: Fare
|
|
Wars, which is a launcher for its two games, stopped at a black screen that way (#19894).
|
|
|
|
## Known leftovers in PPSSPP
|
|
|
|
- `gpu/signals/jumps` and `gpu/signals/simple` ask for a list's state the moment
|
|
`sceGeListUpdateStallAddr` returns. On hardware the list is long done by then, callbacks and
|
|
all. For PPSSPP it has reached its FINISH, but the interrupt is still to come.
|
|
|
|
- `IgnoreEnqueue` in `compat.ini` (Metal Gear Acid 2, #10906) skips the stack check. It was most
|
|
likely hitting the not-started case, which no longer fails. If the game is fine without it,
|
|
remove it.
|
|
- The GE has one stall address, which isn't the same thing as the one each list remembers:
|
|
`sceGeListUpdateStallAddr` always updates the list's, but only a RUNNING list's update reaches
|
|
the GE. PPSSPP has just `DisplayList::stall` for both. The pause window gets away with that,
|
|
since a PAUSED list doesn't run. What doesn't is an old-SDK game updating the stall from inside
|
|
a `HANDLER_SUSPEND` callback, where the list reads as PAUSED: on hardware the GE then stops at
|
|
the *old* stall address until the next update from a thread, while reading as DRAWING rather
|
|
than STALLING. PPSSPP lets it run on. `gpu/signals/handlercalls` shows it, and fails on it.
|
|
Fixing it means giving `GPUCommon` a stall address of its own for execution, which touches the
|
|
fast paths in `GPUCommonHW.cpp` and both GE debuggers.
|
|
- `sceGeBreak(0)` doesn't back up over a half-done SIGNAL or FINISH pair. With lists executing in
|
|
one go, it's rarely in the middle of one.
|
|
- Signal CALL and RET save and restore only the pc, the offset address and the base address. On
|
|
hardware the list's own CALL depth and return addresses are part of it as well.
|
|
- On hardware, a `sceGeListEnQueueHead` that fails with `0x800001FE` still uses up one of the 64
|
|
lists until the next `sceGeBreak(1)`. Not emulated, and not worth it.
|