mirror of
https://github.com/hrydgard/ppsspp.git
synced 2026-10-01 14:58:14 +00:00
docs: describe how the display list queue behaves
What SIGNAL and FINISH interrupts do and in which order, what enqueueing checks, the pause window, what the sync calls wait for, and how ProcessDLQueue() keeps the order right while executing lists in one go. Also a note on gentest.py keeping a pipe open through usbhostfs_pc. Co-Authored-By: Claude Fable 5.1 <[email protected]>
This commit is contained in:
1 parent
33c01a3fc1
commit
41627cc0ce
3 files changed
+200
No files matched your search
@@ -161,6 +161,10 @@ python3 test.py --graphics=software io/shortname/shortname
|
||||
`1.txt` rather than `1.TXT`. Anything testing FAT semantics has to run against `ms0:` - create a
|
||||
scratch directory on the real memory stick and clean it up - and anything reading `host0:` will
|
||||
only reproduce on the OS it was recorded on.
|
||||
- **Start `usbhostfs_pc` yourself before the first `gentest.py` run in a scripted session.** When
|
||||
`gentest.py` has to start it, the bridge inherits the script's stdout and keeps running, so
|
||||
anything waiting for the pipe to close (`gentest.py ... | tail`, or an agent's shell tool) hangs
|
||||
long after the `.expected` file has been written. With the port already open it returns at once.
|
||||
- **A test that hangs leaves the PSP wedged.** `gentest.py` issues `pspsh -e reset` after a
|
||||
timeout, but if you ran the PRX by hand, do that yourself. Default timeout is 10s; raise it with
|
||||
`-t SECONDS`.
|
||||
|
||||
+195
@@ -0,0 +1,195 @@
|
||||
# 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`,
|
||||
`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. |
|
||||
|
||||
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. 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.
|
||||
|
||||
### 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.
|
||||
|
||||
Two 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), and `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.
|
||||
|
||||
## Waiting
|
||||
|
||||
`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. 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.
|
||||
|
||||
`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`.
|
||||
|
||||
## Known leftovers in PPSSPP
|
||||
|
||||
- `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 "Discarding display list with state NONE" workaround in `ProcessDLQueue()` (Crazy Taxi,
|
||||
#19894) guards against a NONE list being on the queue. With the finished list now staying on
|
||||
the queue until its interrupt is done, the known ways for that to happen are gone, but it
|
||||
hasn't been proven unreachable.
|
||||
- `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.
|
||||
Reference in new issue
Block a user