JNEXT
Skip to content

Reading the machine

Everything below can be read in any rule, in its when condition and in a var initial value. Reading never disturbs the machine: a script's read of memory fires no watchpoint and no on read rule, and a NextREG read never triggers the register's own read side effects (NR 0x1E/0x1F, for instance, are read as they are). Every name reads the machine as it is when the rule runs — except PC and CYCLE inside an event rule, which are the event's (see Events).

Most of these names can also be changed with set: see Changing the machine.

Registers and flags

Name What
A B C D E H L F I R 8-bit registers
AF BC DE HL IX IY SP PC 16-bit registers
AF2 BC2 DE2 HL2 the alternate set (AF', BC', DE', HL')
SF ZF HF PF NF CF the flags (sign, zero, half carry, parity/overflow, subtract, carry), 0 or 1
IFF1 IFF2 the interrupt flip-flops, 0 or 1
IM the interrupt mode, 0, 1 or 2
HALTED 1 while the CPU is halted

Memory

Expression What
mem[a] the byte at CPU address a (0 to 0xFFFF), as the CPU sees it now
mem16[a] the little-endian word at a
phys[p, o] the byte at offset o (0 to 0x1FFF) of physical 8K page p, wherever it is paged
stack[n] the word at SP + 2n: stack[0] is the top of the stack

phys[] reads a page whether or not it is mapped; a page that does not exist is a run-time error.

The MMU

Expression What
mmu[s] MMU slot s (0 to 7) as NR 0x50+s reads it: 0xFF when the slot holds ROM
page[s] the physical 8K page slot s holds now

dump_mmu logs all eight, slot:mmu/page.

In a slot holding ROM, page[s] is the ROM's own page number, 0 to 7 — the same numbers as RAM pages 0 to 7. A page 0x00 filter therefore matches ROM code as well as RAM page 0: check mmu[s] != 0xFF if you need RAM only.

NextREGs

nextreg[r] reads NextREG r (0 to 0xFF), without its read side effects.

Time and the raster

Name What
FRAME frames since power-on (or since the last hard reset); the first is 0
CYCLE 28 MHz master-clock cycles since power-on: 8 per T-state at 3.5 MHz, 4 at 7 MHz, … In an event rule, the event's own cycle
TFRAME master cycles since the current frame began
RAW_VC, RAW_HC the hardware's frame counters: line 0 is the top of the whole frame (blanking included), and RAW_HC counts 7 MHz pixels along it
VC_ULA, HC_ULA the ULA's counters: VC_ULA 0 is the first line of the 256×192 paper
CVC the line the Copper, the line interrupt and NR 0x1E/0x1F use: VC_ULA plus the NR 0x64 offset
PHC the pixel column: 0 is the first paper pixel

There is deliberately no plain VC: these counters start at different places, and using one for another is a classic off-by-a-border bug. CVC is the line number a Next programmer reasons in. CVC and HC_ULA step a few pixels into a raw line, not at its start, as on the hardware.

CYCLE, TFRAME and FRAME wrap at 32 bits like every value: CYCLE passes 2³¹ after about 77 seconds of emulated time. A difference (CYCLE - t0) stays right across the wrap; comparing an absolute CYCLE does not. The on cycle N filter itself is 64-bit.

The machine and the mixer

Name What
MACHINE 0 = 48K, 1 = 128K, 2 = +3, 3 = Pentagon, 4 = Next
AUDIO_MUTE the host-side mute mask, the Audio panel's (bit 0 to 2: AY chips 0 to 2, bit 3: the DACs, bit 4: the beeper)

An example

On the 48K, at the first IM 1 interrupt after frame 120:

on execute 0x0038 once when FRAME >= 120 do
    log "BC=${BC:x4} B=${B:x2} C=${C:x2} IX=${IX:x4} IY=${IY:x4} SP=${SP:x4} I=${I:x2} R=${R:x2}"
    log "AF'=${AF2:x4} BC'=${BC2:x4} DE'=${DE2:x4} HL'=${HL2:x4}"
    log "flags: S=${SF} Z=${ZF} H=${HF} P/V=${PF} N=${NF} C=${CF}; IFF1=${IFF1} IFF2=${IFF2} IM=${IM} HALTED=${HALTED}"
    log "stack: ${stack[0]:x4} ${stack[1]:x4} ${stack[2]:x4}; mem16[SP]=${mem16[SP]:x4}"
    log "mmu: ${mmu[0]:x2} ${mmu[1]:x2} ${mmu[2]:x2} ${mmu[3]:x2}; page: ${page[0]:x2} ${page[1]:x2} ${page[2]:x2} ${page[3]:x2}"
    log "phys[0x0A, 0x1C78] = ${phys[0x0A, 0x1C78]} = mem[0x5C78] = ${mem[0x5C78]}"
    log "time: FRAME=${FRAME} CYCLE=${CYCLE} TFRAME=${TFRAME}; MACHINE=${MACHINE} AUDIO_MUTE=${AUDIO_MUTE}"
    dump_mmu
    dump_mem 0x5C78 3
end
on frame 125 do exit 0 end
[debugger] [info] [jds F:120 C:67093160] BC=0000 B=00 C=00 IX=0000 IY=5C3A SP=FF48 I=3F R=43
[debugger] [info] [jds F:120 C:67093160] AF'=0044 BC'=174B DE'=0006 HL'=107F
[debugger] [info] [jds F:120 C:67093160] flags: S=0 Z=1 H=1 P/V=1 N=0 C=0; IFF1=0 IFF2=0 IM=1 HALTED=0
[debugger] [info] [jds F:120 C:67093160] stack: 15EB 0000 15E1; mem16[SP]=15EB
[debugger] [info] [jds F:120 C:67093160] mmu: FF FF 0A 0B; page: 00 01 0A 0B
[debugger] [info] [jds F:120 C:67093160] phys[0x0A, 0x1C78] = 37 = mem[0x5C78] = 37
[debugger] [info] [jds F:120 C:67093160] time: FRAME=120 CYCLE=67093160 TFRAME=680; MACHINE=0 AUDIO_MUTE=0
[debugger] [info] [jds F:120 C:67093160] mmu 0:FF/00 1:FF/01 2:0A/0A 3:0B/0B 4:04/04 5:05/05 6:00/00 7:01/01
[debugger] [info] [jds F:120 C:67093160] 5C78: 25 00 00

IFF1 is 0: accepting the interrupt cleared it. Page 0x0A at offset 0x1C78 is address 0x5C78, because slot 2 (0x4000..0x5FFF) holds page 0x0A.

The raster counters, at an instruction a few lines into the frame:

[debugger] [info] [jds F:101 C:56478296] at 10B0: CVC=252 VC_ULA=252 HC_ULA=289 PHC=277 RAW_VC=4 RAW_HC=406

Raw line 4 is near the top of the frame, in the vertical blanking, which on the 48K comes after the 192 paper lines and the bottom border in CVC's count: so CVC is 252.

C variables: cvar()

on execute main.c:21 once when cvar(i) == 3 do
    log "main.c:21 with i = ${cvar(i)}, table[3] = ${cvar(table[3])}, counter = ${cvar(counter)}"
end

cvar(PATH) reads a C variable of the program, as a 32-bit integer. It needs a z88dk -debug map, which carries the program's C variable records (see Building with z88dk); with the program of A first session and the script above in cv.jds,

jnext --headless --machine next --load prog.nex --script cv.jds --delayed-automatic-exit-frames 700

logs (z88dk 2.4 build; the timestamps and the [debugger] [info] prefixes are left out)

SCRIPT cv.jds: names a source line; it loads when the --load program has loaded
Loaded 19 z88dk symbols from 'prog.map'
Loaded 46 z88dk source lines from '/home/you/prog/prog.map' (program identity not verified: no section binaries beside it)
SCRIPT loaded cv.jds: 1 rules [client 2]
[jds F:500 C:283637187] main.c:21 with i = 3, table[3] = 9, counter = 14 [client 2]

The path is a C name and what follows it:

PATH Reads
i, counter a local or parameter of the frame the CPU is in, else a static of the function, a file static or a global, in that order
sum::calls the static local calls of function sum, from any frame
vars.c::s_dup the file static (or global) of that file: two files can each have an s_dup
vars.c::sum::calls the same, naming the file that holds the function
p->x, g_pt.tag, g_matrix[1][2], *p, *g_cptr a member, an element or a dereference; the index is decimal, 0x or $ hex, and spaces are allowed (p -> x)

What it gives:

  • The frame is the one the CPU is in. There is one atom and no frame argument, so cvar(a) in a rule on sum's line is sum's a. A static local is reachable from any line as sum::calls.
  • Integers and pointers. A signed or unsigned integer of up to 32 bits reads as itself (an unsigned 16-bit 40000 is 40000, not -25536) and a pointer is its address, so mem[cvar(p)] is the byte it points at. An unsigned 32-bit value keeps its bit pattern, as an integer literal does: cvar(g_ulong) == 0xCAFEBABE.
  • What is refused: a struct or an array (name a member or an element), a float (the language has no floats), and an integer wider than 32 bits. The message names the type.
  • A global in a banked page is read from its own page whether or not that page is mapped now.
  • cvar() reads. set cvar(x) = v writes: see Changing the machine.

The path is checked when the script loads: cvar(a.) is an error at the cvar, with the reason. What it names is looked up each time the expression runs, because the map may attach after the script loads. So every other refusal is a run-time error, never a 0: no C variables in the loaded map (an SLD has none), a name that does not exist, a local that is out of scope on the line, a variable the compiler kept nowhere, a null pointer, an index past the end of the array. In a when that disables the rule and the run exits 1:

on execute main.c:21 when cvar(nosuch) == 3 do
    log "never"
end
SCRIPT ERROR cv3.jds:1:27: cvar(nosuch): unknown name 'nosuch' - rule at 1:1 disabled [client 2]
SCRIPT: a run-time error disabled a rule; exiting 1 [client 2]

So put the rule on a line where the variable is in scope, or on execute at a line of the function that has it.

Symbols: @name

on execute @main_loop do …
on write 0x8000..(@__data_crt_head - 1) do …
set mem[@patch_byte] = 0x5A

@name is the address of a symbol from a loaded MAP file: a z88dk .map, an sjasmplus --sym or --exp file, a simple map or NextBuild's Memory.txt, loaded with --map FILE (a z88dk .map or an sjasmplus file, told apart by its contents) or Map ▸ Load MAP File — the same table the debugger's disassembly uses. Symbols are resolved when the script loads: an unknown one is an error then, never a 0 at run time.

[debugger] [error] SCRIPT ERROR err3.jds:1:12: unknown symbol `@no_such_symbol` (not in any loaded MAP)

Both kinds of z88dk MAP entry can be named: ; addr entries (labels) and ; const entries — compile-time constants such as the section bounds __data_crt_head and __code_user_tail. A constant is a value with a name: it resolves as @name but never names an address in the disassembly. A name the MAP defines twice takes its first definition, and a name that is both a label and a constant is the label.

A z88dk C variable or function answers to its C name: @counter for _counter (see Symbols from MAP files). A label or constant really named counter wins over it.

A z88dk symbol of a BANK_n or PAGE_n section is its 16-bit address in the page window, even when the map writes it with the page in the top byte ($14E000 is $E000 as @name). on execute @name fires at that address only while the memory the label belongs to is mapped there (an SLD label of page 20 does not fire when page 30 holds the same address); add page N to choose a page yourself, which then takes its own meaning. A 24-bit address in a section that names no page, and a constant that does not fit 16 bits, are not names.

The name after @ can be any identifier, keywords included (@start), or identifiers joined by dots, the way sjasmplus names a local or a module label: @main.loop, @gfx.draw.loop, @point.x. A . joins only when a letter or _ follows it, so a range such as @main.loop..@main is still a range.

Reading something that is not there

An accessor out of range is a run-time error, which switches the rule off:

on frame 51 do log "mmu[8]=${mmu[8]}" end
on frame 52 do log "phys=${phys[0x1FF, 0]}" end
on frame 60 do exit 0 end
[debugger] [error] SCRIPT ERROR accessors.jds:1:30: `mmu[]` slot 8 is outside 0..7 - rule at 1:1 disabled
[debugger] [error] SCRIPT ERROR accessors.jds:2:28: `phys[]` page 0x1FF cannot be read (invalid_page) - rule at 2:1 disabled
[debugger] [error] SCRIPT: a run-time error disabled a rule; exiting 1
[platform] [info] script requested exit 1