Give the machine a frame to wait for
V3. The screen interrupts at each frame on hardware vector 0x30, and WAIT finally has something worth sleeping on. THERE WAS NO CLOCK. Every program that wanted to happen at a certain speed counted instructions and hoped, which is why Snake's pause silently halved the day a cycle stopped being an instruction and became a memory access - the program was right and the thing it was counting changed underneath it. A screen finishing sixty times a second is a real beat, and it is counted in the MACHINE'S cycles rather than the host's, so the same program sees the same number of frames in the same number of cycles however fast anything really ran. That is what makes a frame something a test can count and a recorded result can hold. Status bit 0 goes up when a frame has gone by and reading the status port puts it down, so a program with no handler can watch for it instead. Control bit 0 asks to be interrupted, and is OFF when the machine starts: an interrupt with nothing installed to catch it is a fault, so a screen that began interrupting the moment it was switched on would take down every program written before frames existed. More than one frame can pass between two looks, and the flag and the line are each one thing, so several still mean one of each. A missed frame is missed. Programs/Examples/frames.asm prints a dot a frame for a second: 1,000,324 cycles, and 996,460 of them spent asleep. That split is the thing worth seeing - a program that polled instead would print the same sixty dots, take the same second, and spend every cycle of it on the bus. Its header explains why waiting is not spinning and why a machine with a beat can stop guessing at one. Six checks in Tests/video.sh, and two of them are about the clock rather than the output, because the output cannot tell the difference. That the machine slept through nearly all of ten frames, and that polling three frames actually took three frames - a status flag that stayed up once set would print exactly the same character and look perfectly correct. Breaking the frame interrupt on purpose left a machine asleep for ever and hung the whole suite, which is a worse way to be told than a failing check. Tests/video.sh bounds its runs at ten seconds now, the way Tests/run.sh always has. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
co-authored by
Claude Opus 5
parent
3da020898c
commit
1174bd9af5
@@ -0,0 +1,97 @@
|
||||
; frames.asm
|
||||
; Waiting for the screen, which is the only regular beat this machine has.
|
||||
; Written by Anachronaut
|
||||
;
|
||||
; ---- There is no clock ----
|
||||
;
|
||||
; Nothing on a SplitBit can tell you how long a second is. Every program that wanted to
|
||||
; happen at a certain speed has counted instructions and hoped - which is why Snake's pause
|
||||
; quietly halved the day a cycle stopped being an instruction and became a memory access.
|
||||
; The program was right; the thing it was counting had changed underneath it.
|
||||
;
|
||||
; A screen finishes drawing sixty times a second, and that is a real beat. It is counted in
|
||||
; the machine's own cycles rather than the host's, so this program sees sixty frames a second
|
||||
; whether the emulator is running at its proper rate or as fast as it possibly can.
|
||||
;
|
||||
; ---- Waiting rather than spinning ----
|
||||
;
|
||||
; WAIT stops the machine until something interrupts it. That is not the same as looping until
|
||||
; a flag goes up, even though both take the same time and print the same thing: a machine in
|
||||
; WAIT is not using memory, so its cycles are counted as idle rather than as bus. Run this
|
||||
; and the last line says so - nearly every cycle it spent, it spent asleep.
|
||||
;
|
||||
; Which is the whole argument for having a frame to wait for. On real hardware that is a
|
||||
; machine that could be doing something else, or nothing at all and drawing less current.
|
||||
|
||||
#Program
|
||||
|
||||
start:
|
||||
SETD.0 Frames
|
||||
RSTA
|
||||
STA.0
|
||||
|
||||
; Ask the screen to interrupt at each frame, then let interrupts in. The screen does not do
|
||||
; this unless it is asked: an interrupt with nothing installed to catch it is a fault, so a
|
||||
; machine that started interrupting on its own would take down every program that had never
|
||||
; heard of frames.
|
||||
INIA 0x01
|
||||
OUTA 0x35
|
||||
SIF
|
||||
|
||||
everyFrame:
|
||||
; A dot a frame, so there is something to watch.
|
||||
INIA 0d46
|
||||
OUTA 0x00
|
||||
|
||||
; And nothing at all until the next one.
|
||||
WAIT
|
||||
|
||||
SETD.0 Frames
|
||||
LDA.0
|
||||
INIB 0d60 ; One second of them
|
||||
CCF
|
||||
SUB
|
||||
BNQ everyFrame
|
||||
|
||||
; Put the screen back the way it was found, and stop asking to be interrupted before
|
||||
; taking away the thing that would catch it.
|
||||
CIF
|
||||
RSTA
|
||||
OUTA 0x35
|
||||
SETD.0 Done
|
||||
RCAL say
|
||||
HALT
|
||||
|
||||
; ---- Called sixty times a second ----
|
||||
;
|
||||
; A handler runs between two instructions of whatever was going on, so it saves everything it
|
||||
; touches - which for an interrupt the machine does itself. RETI puts it all back.
|
||||
frame:
|
||||
SETD.0 Frames
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
RETI
|
||||
|
||||
say:
|
||||
LDA.0
|
||||
BRA sayDone
|
||||
OUTA 0x00
|
||||
INCD.0
|
||||
BRI say
|
||||
sayDone:
|
||||
RRET
|
||||
|
||||
#Data
|
||||
|
||||
Frames:
|
||||
0x00
|
||||
Done:
|
||||
"
|
||||
that was a second
|
||||
"
|
||||
|
||||
#Vectors
|
||||
|
||||
Boot start
|
||||
Device 0x30 frame
|
||||
+52
-2
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "video.h"
|
||||
#include "font.h"
|
||||
#include "io.h"
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
@@ -71,6 +72,12 @@ static int cursorAtColumn = 0;
|
||||
static int cursorVisible = 0;
|
||||
static unsigned long videoNow = 0;
|
||||
|
||||
// When the last frame boundary went by, whether one has gone by unnoticed, and whether the
|
||||
// screen is meant to say so out loud.
|
||||
static unsigned long lastFrame = 0;
|
||||
static int frameWaiting = 0;
|
||||
static int frameInterrupts = 0;
|
||||
|
||||
void videoSetCursor(int row, int column, int visible) {
|
||||
cursorAtRow = row;
|
||||
cursorAtColumn = column;
|
||||
@@ -79,6 +86,19 @@ void videoSetCursor(int row, int column, int visible) {
|
||||
|
||||
void videoTick(unsigned long now) {
|
||||
videoNow = now;
|
||||
// ---- Caught up rather than counted ----
|
||||
//
|
||||
// A loop, because more than one frame can go by between two looks: the machine runs in
|
||||
// batches, and a slow host or a --fast run can cover several frames before anything asks.
|
||||
// The flag and the line are each ONE THING, so several frames at once still mean one of
|
||||
// each - a missed frame is missed, which is what missing one is.
|
||||
while (now - lastFrame >= VIDEO_FRAME_CYCLES) {
|
||||
lastFrame += VIDEO_FRAME_CYCLES;
|
||||
frameWaiting = 1;
|
||||
if (frameInterrupts) {
|
||||
raiseInterrupt(PORT_VIDEO);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void videoLoadFont(void) {
|
||||
@@ -133,6 +153,10 @@ void videoReset(void) {
|
||||
scroll = 0;
|
||||
renderedWidth = 0;
|
||||
renderedHeight = 0;
|
||||
lastFrame = videoNow;
|
||||
frameWaiting = 0;
|
||||
frameInterrupts = 0;
|
||||
clearInterrupt(PORT_VIDEO);
|
||||
// A machine wakes up able to show text. Everything here is ordinary video memory that a
|
||||
// program may overwrite the moment it wants the screen for something else.
|
||||
videoLoadFont();
|
||||
@@ -153,6 +177,16 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
mode = value;
|
||||
}
|
||||
break;
|
||||
case VIDEO_CONTROL:
|
||||
frameInterrupts = (value & VIDEO_CONTROL_FRAME) != 0;
|
||||
if (!frameInterrupts) {
|
||||
// Asking to stop being interrupted takes down whatever was already asked
|
||||
// for. A request that outlived the setting that made it would arrive at a
|
||||
// program which had just said it did not want it - the same reasoning the
|
||||
// console's interrupt bit is written under.
|
||||
clearInterrupt(PORT_VIDEO);
|
||||
}
|
||||
break;
|
||||
case VIDEO_SCROLL:
|
||||
// Wrapped rather than clipped, because the map is a ring and every byte names a
|
||||
// row that exists.
|
||||
@@ -168,8 +202,24 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
||||
|
||||
uint8_t videoRead(uint8_t port) {
|
||||
switch (port) {
|
||||
// Reserved for the frame interrupt, which is the next rung. Zero until then.
|
||||
case VIDEO_STATUS: return 0;
|
||||
case VIDEO_STATUS: {
|
||||
uint8_t status = 0;
|
||||
if (frameWaiting) {
|
||||
status |= VIDEO_STATUS_FRAME;
|
||||
}
|
||||
if (frameInterrupts) {
|
||||
status |= VIDEO_STATUS_INTERRUPT;
|
||||
}
|
||||
// Looking is what answers it. A frame that has been noticed is not still
|
||||
// waiting to be, and a program polling in a loop would otherwise see the first
|
||||
// frame for ever.
|
||||
frameWaiting = 0;
|
||||
return status;
|
||||
}
|
||||
case VIDEO_CONTROL:
|
||||
// Write only. Everything it sets is reported by the status port, and one fact
|
||||
// wants one place to live.
|
||||
return 0;
|
||||
case VIDEO_MODE: return mode;
|
||||
// Asked rather than assumed. A program that wants to know how wide the screen is
|
||||
// should be able to find out, the same way it asks the console what mode it is in.
|
||||
|
||||
@@ -82,6 +82,32 @@
|
||||
#define VIDEO_COLUMNS 0x32
|
||||
#define VIDEO_ROWS 0x33
|
||||
#define VIDEO_SCROLL 0x34
|
||||
#define VIDEO_CONTROL 0x35
|
||||
|
||||
// ---- The frame ----
|
||||
//
|
||||
// A screen finishes drawing sixty times a second and then has a moment before it starts
|
||||
// again, and that moment is the one safe time to change what it is drawing. It is also the
|
||||
// only regular beat this machine has: there is no clock, and every program that wanted to
|
||||
// happen at a certain speed has until now counted instructions and hoped.
|
||||
//
|
||||
// Sixty a second at a megahertz. On the MACHINE'S clock rather than the host's, so a program
|
||||
// runs the same number of frames in the same number of cycles however fast anything really
|
||||
// went - which is what makes a frame something a test can count.
|
||||
#define VIDEO_FRAME_CYCLES 16667
|
||||
|
||||
// Set when a frame has gone by, and cleared by reading the status port. A program with no
|
||||
// handler installed can wait on this instead, the way a program can poll the console rather
|
||||
// than being interrupted by it.
|
||||
#define VIDEO_STATUS_FRAME 0x01
|
||||
// Whether the screen is set to interrupt, so that a program can ask what it asked for.
|
||||
#define VIDEO_STATUS_INTERRUPT 0x02
|
||||
|
||||
// Asks to be interrupted at each frame, on hardware vector 0x30. OFF WHEN THE MACHINE
|
||||
// STARTS, because an interrupt with nothing installed to catch it is a fault, and a machine
|
||||
// that began interrupting the moment it was switched on would take any program that had not
|
||||
// thought about frames down with it.
|
||||
#define VIDEO_CONTROL_FRAME 0x01
|
||||
|
||||
void videoReset(void);
|
||||
|
||||
|
||||
@@ -550,11 +550,12 @@ The high nibble is reserved and should be left at zero, so that a meaning can be
|
||||
|
||||
| Port | Register |
|
||||
| --- | --- |
|
||||
| 0x30 | Status. Reserved for the frame interrupt, and reads zero until there is one. |
|
||||
| 0x30 | Status. Bit 0 a frame has gone by, bit 1 the screen is set to interrupt. |
|
||||
| 0x31 | Mode. |
|
||||
| 0x32 | Columns, read only. |
|
||||
| 0x33 | Rows, read only. |
|
||||
| 0x34 | Scroll. |
|
||||
| 0x35 | Control. Bit 0 asks to be interrupted at each frame. |
|
||||
|
||||
| Mode | Screen | Cells |
|
||||
| --- | --- | --- |
|
||||
@@ -565,6 +566,32 @@ Both are 8 by 8 cells over the same engine, and the pixel count costs a program
|
||||
|
||||
How big the screen is, is asked for rather than assumed. A program written once can find out what it is running on.
|
||||
|
||||
### The Frame:
|
||||
|
||||
A screen finishes drawing sixty times a second and then has a moment before it starts again. That moment is the one safe time to change what it is drawing - and it is also **the only regular beat this machine has.** There is no clock here. Every program that wanted to happen at a certain speed has until now counted instructions and hoped, which is why Snake's pause quietly halved the day a cycle stopped being an instruction and became a memory access.
|
||||
|
||||
Sixty a second, counted in the machine's own cycles rather than the host's. So a program sees the same number of frames in the same number of cycles however fast anything really ran, which is what makes a frame something a test can count and a recorded result can contain.
|
||||
|
||||
**Status bit 0 goes up when a frame has gone by, and reading the status port puts it down.** Looking is what answers it: a frame that has been noticed is not still waiting to be noticed, and a program polling in a loop would otherwise see the first frame for ever.
|
||||
|
||||
**Control bit 0 asks to be interrupted instead**, on hardware vector 0x30, which is the screen's base port. It is **off when the machine starts**, and that is not caution for its own sake: an interrupt with nothing installed to catch it is a fault, so a screen that began interrupting the moment it was switched on would take down every program written before frames existed. Asking to stop takes down any request already standing, for the same reason the console's interrupt bit does.
|
||||
|
||||
More than one frame can go by between two looks - the machine runs in batches, and a slow host covers several at once. The flag and the line are each one thing, so several frames still mean one of each. **A missed frame is missed**, which is what missing one means.
|
||||
|
||||
This is what `WAIT` was built for. A program does its work, waits, and is woken:
|
||||
|
||||
```
|
||||
INIA 0x01
|
||||
OUTA 0x35 ; Interrupt me at each frame
|
||||
SIF
|
||||
loop:
|
||||
; ... draw ...
|
||||
WAIT ; Nothing to do until the screen says so
|
||||
BRI loop
|
||||
```
|
||||
|
||||
A machine doing that is asleep between frames rather than spinning, and the difference is visible: the cycles it spent are counted as idle rather than as bus, so a program that waited properly and one that polled in a loop can be told apart even though they print the same thing and take the same time.
|
||||
|
||||
### Scrolling:
|
||||
|
||||
**The map is a ring, and the Scroll register says which of its 128 rows is drawn at the top.** Screen row *r* shows map row *scroll + r*, wrapped.
|
||||
|
||||
@@ -78,7 +78,7 @@ from `make`, not from here.
|
||||
### 1. Recorded output
|
||||
|
||||
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
|
||||
everything it printed against a file in `Tests/expected`. 167 tests, of which 105 run, 35
|
||||
everything it printed against a file in `Tests/expected`. 168 tests, of which 106 run, 35
|
||||
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
|
||||
given at all.
|
||||
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
............................................................
|
||||
that was a second
|
||||
Execution halted.
|
||||
[exit 0]
|
||||
@@ -45,6 +45,10 @@ printHello | Examples/printHello.asm | run | -
|
||||
16bitFibonacci | Examples/Fibonacci/16bitFibonacci.asm | run | - | -
|
||||
32bitFibonacci | Examples/Fibonacci/32bitFibonacci.asm | run | - | -
|
||||
colours | Examples/colours.asm | run | - | -
|
||||
# frames waits for the screen sixty times, which is one second at the emulated rate. It is
|
||||
# here because the count is exact: a frame is counted in the machine's own cycles, so the
|
||||
# same program sees the same sixty however fast the host really went.
|
||||
frames | Examples/frames.asm | run | - | -
|
||||
8bitSieve | Examples/primeSieve/8bitSieve.asm | run | - | -
|
||||
16bitSegmentedSieve | Examples/primeSieve/16bitSieve.asm | run | - | -
|
||||
# The four pointer rewrite. It emits exactly the same primes as the line above, which
|
||||
|
||||
+145
-3
@@ -124,11 +124,21 @@ run() {
|
||||
cat > "$BUILD/$name.asm"
|
||||
"$ASM" "$BUILD/$name.asm" -o "$BUILD/$name.bin" >"$BUILD/$name.log" 2>&1 || {
|
||||
echo "could not assemble $name"; sed 's/^/ /' "$BUILD/$name.log"; return 1; }
|
||||
# ---- Bounded, the way run.sh bounds things ----
|
||||
#
|
||||
# A program here can WAIT for something that never comes, and one did: breaking the frame
|
||||
# interrupt on purpose left a machine asleep for ever and took the whole suite with it,
|
||||
# which is a worse way to be told than a failing check. Ten seconds, and a test that hangs
|
||||
# says so instead of hanging.
|
||||
if [ -n "${2:-}" ]; then
|
||||
"$EMU" --fast --keyboard "$2" --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" \
|
||||
> "$BUILD/$name.out" 2>&1
|
||||
timeout 10 "$EMU" --fast --keyboard "$2" --screen "$BUILD/$name.ppm" \
|
||||
"$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1
|
||||
else
|
||||
"$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" > "$BUILD/$name.out" 2>&1
|
||||
timeout 10 "$EMU" --fast --screen "$BUILD/$name.ppm" "$BUILD/$name.bin" \
|
||||
> "$BUILD/$name.out" 2>&1
|
||||
fi
|
||||
if [ $? -eq 124 ]; then
|
||||
echo " $name did not finish within ten seconds"
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -525,6 +535,138 @@ coloured blinkagain 0 0 "216,216,216" \
|
||||
&& result ok "and lit again after that" "which is what blinking is" \
|
||||
|| result no "and lit again after that" "got $(pixel blinkagain 0 0)"
|
||||
|
||||
# ---- The frame, which is the only beat this machine has ----
|
||||
#
|
||||
# There is no clock. Every program that wanted to happen at a certain speed has until now
|
||||
# counted instructions and hoped, which is why Snake's pause silently halved when a cycle
|
||||
# stopped being an instruction. A screen finishing sixty times a second is a real one, and it
|
||||
# arrives on the MACHINE'S clock, so the same program sees the same number of frames in the
|
||||
# same number of cycles however fast the host really went.
|
||||
|
||||
FRAMER='#Program
|
||||
start:
|
||||
SETD.0 Frames
|
||||
RSTA
|
||||
STA.0
|
||||
INIA 0x01
|
||||
OUTA 0x35
|
||||
SIF
|
||||
loop:
|
||||
WAIT
|
||||
SETD.0 Frames
|
||||
LDA.0
|
||||
INIB 0d10
|
||||
CCF
|
||||
SUB
|
||||
BRQ done
|
||||
BRI loop
|
||||
done:
|
||||
SETD.0 Frames
|
||||
LDA.0
|
||||
INIB 0d48
|
||||
CCF
|
||||
ADD
|
||||
MVQA
|
||||
OUTA 0x00
|
||||
HALT
|
||||
frame:
|
||||
SETD.0 Frames
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
RETI
|
||||
#Data
|
||||
Frames:
|
||||
0x00
|
||||
#Vectors
|
||||
Boot start
|
||||
Device 0x30 frame'
|
||||
|
||||
echo "$FRAMER" | run frames || exit 1
|
||||
[ "$(said frames)" = "58" ] \
|
||||
&& result ok "the screen interrupts once a frame" "ten of them, counted" \
|
||||
|| result no "the screen interrupts once a frame" "got $(said frames)"
|
||||
|
||||
# ---- And the machine was ASLEEP for them ----
|
||||
#
|
||||
# Which is the whole point of having a frame to wait for, and the one thing the picture
|
||||
# cannot show. Ten frames is 166,670 cycles and the program does a few hundred cycles of work
|
||||
# in them; a machine spinning on the status port instead would show the same characters, take
|
||||
# the same time, and spend every cycle of it on the bus.
|
||||
IDLE="$(grep -oE '[0-9]+ of them waiting' "$BUILD/frames.out" | grep -oE '^[0-9]+')"
|
||||
TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/frames.out" | grep -oE '[0-9]+')"
|
||||
[ -n "$IDLE" ] && [ "$IDLE" -gt $(( TOTAL - TOTAL / 50 )) ] \
|
||||
&& result ok "and slept through nearly all of it" "$IDLE of $TOTAL cycles idle" \
|
||||
|| result no "and slept through nearly all of it" "$IDLE of $TOTAL cycles idle"
|
||||
|
||||
# Nothing is asked for, so nothing arrives - and that matters more than it sounds. An
|
||||
# interrupt with no handler installed is a fault, so a screen that interrupted whether or not
|
||||
# it was asked would take down every program written before frames existed.
|
||||
UNARMED='#Program
|
||||
start:
|
||||
SIF
|
||||
INIA 0d100
|
||||
spin:
|
||||
DECA
|
||||
BNA spin
|
||||
INIA 0d65
|
||||
OUTA 0x00
|
||||
HALT
|
||||
#Vectors
|
||||
Boot start'
|
||||
echo "$UNARMED" | run unarmed || exit 1
|
||||
[ "$(said unarmed)" = "65" ] \
|
||||
&& result ok "and none arrives unless asked for" "no handler, no fault" \
|
||||
|| result no "and none arrives unless asked for" "got $(said unarmed)"
|
||||
|
||||
# A program with no handler can watch for the frame instead, the way one can poll the console
|
||||
# rather than being interrupted by it.
|
||||
POLLER='#Program
|
||||
start:
|
||||
SETD.0 Seen
|
||||
RSTA
|
||||
STA.0
|
||||
poll:
|
||||
INA 0x30
|
||||
INIB 0x01
|
||||
AND
|
||||
BRQ poll
|
||||
SETD.0 Seen
|
||||
LDA.0
|
||||
INCA
|
||||
STA.0
|
||||
INIB 0d3
|
||||
CCF
|
||||
SUB
|
||||
BNQ poll
|
||||
SETD.0 Seen
|
||||
LDA.0
|
||||
INIB 0d48
|
||||
CCF
|
||||
ADD
|
||||
MVQA
|
||||
OUTA 0x00
|
||||
HALT
|
||||
#Data
|
||||
Seen:
|
||||
0x00
|
||||
#Vectors
|
||||
Boot start'
|
||||
echo "$POLLER" | run poller || exit 1
|
||||
[ "$(said poller)" = "51" ] \
|
||||
&& result ok "or watch for it without one" "three frames, polled" \
|
||||
|| result no "or watch for it without one" "got $(said poller)"
|
||||
|
||||
# ---- And looking is what answers it ----
|
||||
#
|
||||
# Three frames polled have to have TAKEN three frames. A flag that stayed up once it was
|
||||
# first set would let this loop through all three without a frame going by, print exactly the
|
||||
# same character, and look perfectly correct - so the count is not the check, the clock is.
|
||||
TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/poller.out" | grep -oE '[0-9]+')"
|
||||
[ "$TOTAL" -gt 33334 ] \
|
||||
&& result ok "and the flag comes down when looked at" "$TOTAL cycles, so three frames passed" \
|
||||
|| result no "and the flag comes down when looked at" "$TOTAL cycles, too few to be three frames"
|
||||
|
||||
# And what scrolled off the top is still in the map, which is scrollback nothing had to keep.
|
||||
{ printf '#Program\nstart:\n'
|
||||
say "A"
|
||||
|
||||
Reference in New Issue
Block a user