Debugger expressions

Almost every place in the PPSSPP debugger where you can type something that isn't a plain number accepts an expression: a small C-like formula that is evaluated against the current state of the emulated machine. Expressions are how you say "break here, but only when a0 is this pointer", "watch the third field of that struct", or "jump to whatever ra points at".

There is one shared expression evaluator, but it comes in two flavours with different sets of built-in variables: the CPU flavour, used by the main debugger, and the GE flavour, used by the graphics (display list) debugger. The syntax is identical, only the names differ.

Quick examples

If you just want a breakpoint condition and don't want to read the whole page, these cover most of it. Remember that numbers are hex by default, so write 0d3 when you mean three.

ExpressionBreaks when
a0 == 0d3The first argument is 3
a0 == 10The first argument is 0x10, that is 16 - not ten
a0 == 0The first argument is a null pointer
a0 == 08801234The first argument is that exact address
ra == 08900abcThe call came from that specific caller
v0 != 0The function is about to return something non-zero
[a0]The 32-bit word a0 points at is non-zero
[a0+8] == 1The field at offset 8 of the struct in a0 is 1
[a0+24,2] > 0d1000A 16-bit field at offset 0x24 exceeds 1000
a0 == 0 && a1 > 0d16Both conditions hold
f12 > 100.0The float in f12 is over 100
threadid == 0d321Only this thread reaches here
flipcount > 0d120Only after 120 more frames have been presented

The same expressions work in a watch, where the result is simply displayed instead of compared.

Where expressions can be used

PlaceFlavourNotes
Breakpoint conditionsCPUExecution, memory and register breakpoints. The breakpoint only trips when the expression evaluates to non-zero.
Breakpoint log formatCPUThe {...} parts of the log text, see Log format strings.
Watch windowCPURe-evaluated continuously, and displayed as hex, decimal, float or string.
Struct viewer watchesCPUThe expression produces the address the struct is read from. Tick "Dynamic" to re-evaluate it every frame.
Address and size fieldsCPU"Go to address" in the disassembly and memory views, the memory dump dialog, the symbol editor, and the address/size fields of the breakpoint dialog.
Register editingCPUTyping a value into the register list, and the register=expression form in the assemble box.
GE display list breakpointsGEConditions on display list addresses and on GE commands. Currently only exposed in the Windows GE debugger.
GE "go to address"GEThe display list view's address field.
WebSocket cpu.evaluateCPUEvaluates an expression and returns it as both an unsigned integer and a float.
WebSocket breakpoint commandsCPUThe condition and logFormat parameters.

The debugger also builds expressions internally. "Run to cursor" plants a temporary breakpoint conditioned on flipcount > N so it only counts hits from the next frame onwards, and stepping conditions its temporary breakpoint on threadid == ... so another thread running through the same address can't complete your step.

Values and types

Everything is a 32-bit value. There is no separate boolean type: comparisons produce 1 or 0, and a condition counts as true when the result is non-zero. There is no string type either; a string is just an address, which the :s log format or the string watch format then reads from memory.

Floats exist, but only barely - see Floating point below.

Numbers

Numbers are hexadecimal by default. 10 means 16. This trips up everyone at least once, so write the radix out when it matters.

FormExampleValue
Bare1016, hex is the default
0x prefix0x1016
0d prefix0d1010, a PPSSPP invention since bare numbers are hex
0b prefix0b101010
0o prefix0o1715
h suffix10h16
i or u suffix10i10
o suffix12o10

A number has to start with a digit, so a hex literal beginning with a letter needs a prefix: write 0xff or 0ff, not ff. There is no binary suffix, because b is a valid hex digit and the default radix is hex - 101b is the hex number 0x101b, not binary. Use the 0b prefix instead.

A token containing a single . surrounded by digits is a float: 1.5, 0.25. Floats are always decimal.

Names

A name starts with a letter or @ and may contain letters, digits, _, @, $ and .. Names are case-insensitive and are resolved in two steps: first against the built-in variables for the current flavour, and only then against the symbol map - the labels loaded from the game's modules, from a .sym file, or created by hand in the debugger. A built-in name therefore shadows a label of the same name.

A name that resolves to neither is an error: Invalid symbol "foo".

CPU variables

NameMeaning
r0-r31General purpose registers by number
zero, at, v0-v1, a0-a3, t0-t9, s0-s7, k0-k1, gp, sp, fp, raThe same registers by their conventional names
f0-f31FPU registers, as floats
fi0-fi31The same FPU registers, as raw 32-bit integers
s000-s733VFPU registers in single notation, as floats
vi0-vi127The same VFPU registers by index, as raw integers
pcProgram counter
hi, loThe multiply/divide result registers
threadidThe currently running thread's ID
moduleidThe module ID the current thread belongs to
usecEmulated time in microseconds, truncated to 32 bits
ticksCPU ticks since boot, truncated to 32 bits
vcountThe PSP's vblank counter, as sceDisplayGetVcount returns it
flipcountFrames actually presented to the screen

vcount and flipcount are both useful for "only from now on" conditions, and they are not interchangeable. A game rendering at 30fps produces two vblanks per frame, so a vcount-based condition lets you through halfway into the frame you were trying to skip. flipcount only advances when the framebuffer actually changed, which is exactly what you want - but it also means it never advances at all if the game has stopped drawing, or is wedged in the very loop you are debugging.

Reading memory

Square brackets read the emulated PSP's memory.

FormMeaning
[address]Read 32 bits
[address,size]Read size bytes, where size is 1, 2 or 4

The address comes first and the size second, which is the opposite order from most assemblers. Reads are little-endian and zero-extended, so [sp,1] is the byte at the stack pointer.

Reads from unmapped addresses quietly return 0 instead of failing. That is deliberate: a breakpoint condition like [a0+4] == 1 has to be configurable while a0 still holds something meaningless, long before the breakpoint is ever reached. The flip side is that a typo'd address looks like a valid zero rather than an error.

Any size other than 1, 2 or 4 is an error.

Operators

All of these behave as in C, and precedence is the same as in C. Listed tightest-binding first:

OperatorsMeaning
( )Grouping
[ ]Memory read
+ - ~ !Unary plus/minus, bitwise not, logical not
* / %Multiply, divide, modulo
+ -Add, subtract
<< >>Shift left, shift right
< <= > >=Comparisons
== !=Equality
&Bitwise and
^Bitwise xor
|Bitwise or
&&Logical and
||Logical or
? :Conditional

Arithmetic is unsigned and wraps at 32 bits, so -1 is 0xffffffff and >> is always a logical shift - there is no arithmetic shift and no signed comparison. Division or modulo by zero is an error rather than a crash. Unlike C, && and || do not short-circuit: both sides are always evaluated, which matters if one of them reads memory.

Nested conditionals need parentheses. 1 ? 2 : 3 ? 4 : 5 evaluates to 4, not 2. Write 1 ? 2 : (3 ? 4 : 5) and it behaves as expected.

Floating point

Float support is real but shallow, and it is easy to get meaningless numbers out of it. The evaluator has no per-value types - it decides once per expression whether that expression is a float expression, and it decides yes if any float constant or float register appears anywhere in it. Every operand in the whole expression is then reinterpreted as a float, bit pattern and all.

So a0 + 1.5 does not add 1.5 to a pointer. It reinterprets the pointer's bits as a float and adds 1.5 to that, which is nonsense. Equality is worse: == and != always compare raw bits, so 1 == 1.0 is false.

On top of that, the result of float arithmetic is truncated back to an integer. If f0 holds 2.5, then f0 * 2.0 gives 5, and f0 + 0.5 gives 3.

What does work well, and is what floats are really there for:

  • A float register on its own. f12 yields the register's raw bits, which the :f log format and the float watch format then display correctly as a float.
  • Comparing float registers against float constants, as in f12 > 100.0. Comparisons other than == and != do compare as floats.

For anything else, prefer fi12 and integer arithmetic on the bit pattern.

Log format strings

A breakpoint can log a line instead of - or as well as - pausing. The log text is a template: everything outside braces is copied literally, and each {expression} is evaluated and substituted. An optional : and a format letter at the end of the braces picks the formatting.

SuffixOutput
:xEight-digit hex. The default when no suffix is given.
:dSigned decimal
:fFloat, from the value's bit pattern
:pAs a pointer: the address in hex, followed by the 32-bit value at that address in brackets, or [invalid]
:sThe zero-terminated string at that address, or (invalid)

For example:

sceIoOpen name={a0:s} flags={a1:x} fd={v0:d}

An empty {} produces a literal {}, which is the only way to get a brace through. A { without a matching } makes the whole format invalid, and the dialog will refuse it. Note that {a0 ? 1 : 2} works - a trailing :2 is not one of the format letters, so it is left alone as part of the ternary.

The same format strings are available through the WebSocket debugger's logFormat parameter.

GE debugger variables

Display list breakpoints and the GE debugger's address fields use the same syntax with a completely different set of names. Memory reads, numbers and operators all work the same way.

GE registers by name. Every GE command can be referenced by its name, for instance vertextype, framebufptr, alphatest or prim. The value is the low 24 bits of the command, or the float bits for commands that hold floats.

Bit fields. Many commands can be broken up with a ., so you don't have to shift and mask by hand: prim.type, prim.count, vertextype.pos, texsize0.width, alphatest.func. The available field names depend on the command's format.

Named constants. The GE enumerations are available as symbols, both in their full form and in a short form: GE_PRIM_TRIANGLES or TRIANGLES, GE_TFMT_5650, GE_COMP_GEQUAL or GEQUAL, GE_TFILT_LINEAR or LINEAR. This makes conditions readable:

prim.type == TRIANGLES && prim.count > 100

Display list state.

NameMeaning
pcCurrent display list PC
stallThe list's stall address
opThe full 32-bit command word at the PC
dataThe low 24 bits of that command word
vaddr, iaddrCurrent vertex and index addresses
offsetThe current offset address
bflag / boundflagResult of the last bounding box test
clutaddrFull CLUT address
texaddr0-texaddr7Full texture addresses per mip level
transfersrc, transferdstBlock transfer source and destination
primcount, lastprimcountPrimitives drawn this frame and last frame

Matrices, as individual float elements: bone0-bone95, world0-world11, view0-view11, proj0-proj15, and tgen0-tgen11 (also spelled texgen0).

When a command breakpoint's condition runs, the register being written already holds the new value, so a condition like texaddr0 == 04000000 sees the address the command is about to set rather than the previous one.

Evaluating expressions from a script

The WebSocket debugger exposes the CPU evaluator directly, which is handy for automation:

{"event": "cpu.evaluate", "expression": "[sp+10,4]"}

The reply carries uintValue and a floatValue string. See the WebSocket debugger documentation for the full protocol.

Examples

Break only on one caller:

ra == 08900abc

Break when a struct field crosses a threshold, reading a 16-bit field:

[a0+24,2] > 1000

Break the first time a function is called with a null pointer, only on the main thread:

a0 == 0 && threadid == 0x123

Skip the first frame of a hot path, then break every time:

flipcount > 0d120

Log the arguments of a call without stopping:

open path={a0:s} mode={a1:x} from={ra:x}

Watch a pointer chain in the watch window - two dereferences, with a field offset:

[[08801234]+10]

GE: break on a large triangle batch drawn with a specific texture format:

prim.type == TRIANGLES && prim.count > 0d200 && texformat.format == GE_TFMT_CLUT8