Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were missing and this adds them: a column origin so the map can be wider than the screen as well as taller, and a pixel remainder for each axis so the step can be one pixel rather than eight. 0x36 Scroll column, in cells, wrapping at 128 0x37 Fine X, 0 to 7 pixels 0x38 Fine Y, 0 to 7 pixels FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0, because only its low three bits mean anything. The alternative was for a write of 8 to step the coarse register, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic itself. It is also what the machines this one is pretending to be did. The renderer now draws one more row and one more column than fit and clips them, because with a fine offset the screen no longer begins on a cell boundary and the cells at two edges are partly off it. videoPutCell follows the column origin as it has always followed the row - a caller means a cell of the SCREEN, and the screen is a window onto the map. The fine offsets are deliberately not applied there: they move the finished picture by less than a cell, and there is no such thing as less than a cell to write into. So a program may scroll to any pixel without the console's idea of where row three, column five is moving underneath it. Grid now scrolls diagonally, a pixel a frame, in four port writes and two carries. It moved eight pixels every fourth frame before, which reads as the picture jumping rather than travelling. Seven checks, each one the same program with one register changed, so what is compared is where the picture stopped. Breaking fine X, fine Y, the column origin, the three-bit mask, or the console's use of the origin each fails exactly one of them. Grid's own two checks had to be rewritten, and the reason is worth keeping: they asked whether pixel 4 was a grid line, which was really a check that the scroll happened to be at a cell boundary. A picture that moves a pixel a frame can only be asked things that are true at every offset - that it repeats every eight pixels, and that one band of eight rows holds different colours from the next. Also repairs docs.sh, which found the minimal CosmOS application by taking the first asm block in the README. Documenting a program with an example above it made that a different block, and the check complained that the minimal application had no #Base about something that never claimed to be one. It looks under System Services now. 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
848103f5e4
commit
bcd42e75ca
@@ -48,13 +48,9 @@ start:
|
|||||||
|
|
||||||
; ---- The loop ----
|
; ---- The loop ----
|
||||||
;
|
;
|
||||||
; Wait for the screen to finish a frame, count four of them, move the origin down one row.
|
; A frame, then one pixel down and one across. This used to move a whole cell every fourth
|
||||||
; That is fifteen rows a second, which is slow enough to watch and fast enough to look
|
; frame, because a cell was as fine as the screen could be moved - eight pixels at a time,
|
||||||
; deliberate.
|
; which reads as the picture jumping rather than travelling.
|
||||||
RSTA
|
|
||||||
SETD.0 Ticks
|
|
||||||
STA.0
|
|
||||||
|
|
||||||
everyFrame:
|
everyFrame:
|
||||||
CALL waitFrame
|
CALL waitFrame
|
||||||
|
|
||||||
@@ -65,30 +61,49 @@ everyFrame:
|
|||||||
AND
|
AND
|
||||||
BNQ finished
|
BNQ finished
|
||||||
|
|
||||||
SETD.0 Ticks
|
; ---- A pixel down, and the cell it belongs to ----
|
||||||
|
;
|
||||||
|
; Fine is the low three bits of the register and does not carry, so this does: eight steps
|
||||||
|
; inside the cell and then one step of the origin. The AND is both the wrap and the test -
|
||||||
|
; Q coming out as nought is exactly the moment the cell boundary was crossed.
|
||||||
|
SETD.0 FineDown
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
STA.0
|
INIB 0x07
|
||||||
INIB 0d4
|
AND
|
||||||
CCF
|
STQ.0
|
||||||
SUB
|
OUTQ 0x38
|
||||||
BNQ everyFrame
|
BNQ stepAcross
|
||||||
|
|
||||||
RSTA
|
; The map is 128 rows against a screen of 25 or 50, so the origin walks a ring: what leaves
|
||||||
STA.0 ; DP0 is still Ticks, from the count just above.
|
; the top has not gone anywhere and comes back round.
|
||||||
|
SETD.0 OriginDown
|
||||||
; ---- The scroll itself ----
|
|
||||||
;
|
|
||||||
; One byte. The map is 128 rows and the screen shows 25 of them, so this walks the origin
|
|
||||||
; through a ring: what leaves the top has not gone anywhere and comes back round.
|
|
||||||
SETD.0 Origin
|
|
||||||
LDA.0
|
LDA.0
|
||||||
INCA
|
INCA
|
||||||
INIB 0x7F
|
INIB 0x7F
|
||||||
AND
|
AND
|
||||||
MVQA
|
STQ.0
|
||||||
STA.0
|
OUTQ 0x34
|
||||||
OUTA 0x34
|
|
||||||
|
stepAcross:
|
||||||
|
; And the same sideways, which is the axis that did not exist at all until now. There are
|
||||||
|
; 128 columns against the 80 shown, so this ring is shallower but it is the same ring.
|
||||||
|
SETD.0 FineAcross
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x37
|
||||||
|
BNQ everyFrame
|
||||||
|
|
||||||
|
SETD.0 OriginAcross
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x7F
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x36
|
||||||
BRI everyFrame
|
BRI everyFrame
|
||||||
|
|
||||||
finished:
|
finished:
|
||||||
@@ -97,11 +112,16 @@ finished:
|
|||||||
|
|
||||||
; ---- Putting the screen back ----
|
; ---- Putting the screen back ----
|
||||||
;
|
;
|
||||||
; The origin first, then every cell of the map and not just the visible ones. A console
|
; All four scroll registers, or the shell inherits a view that begins half way into a cell.
|
||||||
; that scrolls would otherwise walk down into rows this program filled, and find a grid
|
|
||||||
; underneath its own output.
|
|
||||||
RSTA
|
RSTA
|
||||||
OUTA 0x34
|
OUTA 0x36
|
||||||
|
OUTA 0x37
|
||||||
|
OUTA 0x38
|
||||||
|
|
||||||
|
; The row origin too, and then every cell of the map and not just the visible ones. A
|
||||||
|
; console that scrolls would otherwise walk down into rows this program filled, and find a
|
||||||
|
; grid underneath its own output.
|
||||||
|
OUTA 0x34 ; A is still nought, from the three above.
|
||||||
|
|
||||||
INIA 0d3
|
INIA 0d3
|
||||||
OUTA 0xE3
|
OUTA 0xE3
|
||||||
@@ -365,9 +385,13 @@ everyCell:
|
|||||||
|
|
||||||
#Base 0x2000
|
#Base 0x2000
|
||||||
|
|
||||||
Ticks:
|
FineDown:
|
||||||
0x00
|
0x00
|
||||||
Origin:
|
FineAcross:
|
||||||
|
0x00
|
||||||
|
OriginDown:
|
||||||
|
0x00
|
||||||
|
OriginAcross:
|
||||||
0x00
|
0x00
|
||||||
Scheme:
|
Scheme:
|
||||||
0x00
|
0x00
|
||||||
|
|||||||
@@ -531,7 +531,7 @@ from every assembly file in it. Several are old programs written for the bare ma
|
|||||||
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
||||||
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
||||||
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
||||||
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls by writing one byte a frame. |
|
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
|
||||||
| Edit | A line editor. |
|
| Edit | A line editor. |
|
||||||
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
||||||
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
||||||
@@ -685,10 +685,29 @@ use when the thing being debugged is the boot chain, since it skips the boot cha
|
|||||||
Everything else drawn on this machine has been text or a bitmap. `Grid` is the first program
|
Everything else drawn on this machine has been text or a bitmap. `Grid` is the first program
|
||||||
to use the tile engine as an engine, and it is worth reading for the size of the numbers.
|
to use the tile engine as an engine, and it is worth reading for the size of the numbers.
|
||||||
|
|
||||||
It writes **one byte a frame** to scroll. The map is 128 rows and the screen shows 25, so the
|
It scrolls **a pixel a frame, diagonally**, and the whole of that is four port writes and two
|
||||||
rows above and below are already drawn - scrolling moves the origin rather than 2,000 bytes of
|
carries. The map is 128 rows and 128 columns against a screen of 50 and 80, so the cells
|
||||||
screen, and the rows that leave the top have not gone anywhere. A screenful of movement costs
|
around the edge are already drawn - scrolling moves the origin rather than 2,000 bytes of
|
||||||
one `OUTA`.
|
screen, and what leaves the top has not gone anywhere.
|
||||||
|
|
||||||
|
The coarse registers move a whole cell and the fine ones move the remainder, and **they do not
|
||||||
|
carry into each other**, so the program does:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
SETD.0 FineDown
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
OUTQ 0x38
|
||||||
|
BNQ stepAcross ; Still inside the cell.
|
||||||
|
; ... and here, one step of the row origin.
|
||||||
|
```
|
||||||
|
|
||||||
|
The `AND` is both the wrap and the test: `Q` coming out as nought is exactly the moment the
|
||||||
|
cell boundary was crossed. It moved eight pixels every fourth frame before the fine registers
|
||||||
|
existed, which reads as the picture jumping rather than travelling.
|
||||||
|
|
||||||
It puts its tile at **200**, because the machine wakes with the font in tile memory - glyph n
|
It puts its tile at **200**, because the machine wakes with the font in tile memory - glyph n
|
||||||
at tile n, for 135 of the 256 - so a program that starts at zero paints over the alphabet and
|
at tile n, for 135 of the 256 - so a program that starts at zero paints over the alphabet and
|
||||||
|
|||||||
+66
-10
@@ -24,6 +24,13 @@ static uint8_t mode;
|
|||||||
// which is where the console gets scrollback it never had.
|
// which is where the console gets scrollback it never had.
|
||||||
static uint8_t scroll;
|
static uint8_t scroll;
|
||||||
|
|
||||||
|
// The column origin, and the pixel remainder for each axis. Kept apart from the row origin
|
||||||
|
// above rather than folded into it, because they are read at different moments: the origins
|
||||||
|
// decide which cell a program's writes land in, and the fine offsets decide nothing at all
|
||||||
|
// except where the finished picture sits. See videoPutCell.
|
||||||
|
static uint8_t scrollColumn;
|
||||||
|
static uint8_t fineX, fineY;
|
||||||
|
|
||||||
static uint8_t pixels[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT * 3];
|
static uint8_t pixels[VIDEO_MAX_WIDTH * VIDEO_MAX_HEIGHT * 3];
|
||||||
static int renderedWidth = 0;
|
static int renderedWidth = 0;
|
||||||
static int renderedHeight = 0;
|
static int renderedHeight = 0;
|
||||||
@@ -140,9 +147,19 @@ void videoLoadFont(void) {
|
|||||||
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
void videoPutCell(int screenRow, int column, uint8_t tile, uint8_t attribute) {
|
||||||
if (screenRow < 0 || screenRow >= rowsFor(mode)) return;
|
if (screenRow < 0 || screenRow >= rowsFor(mode)) return;
|
||||||
if (column < 0 || column >= columnsFor(mode)) return;
|
if (column < 0 || column >= columnsFor(mode)) return;
|
||||||
|
// ---- Where the caller means, not where the map begins ----
|
||||||
|
//
|
||||||
|
// Both origins, because a caller says "row three, column five OF THE SCREEN" and the
|
||||||
|
// screen is a window onto the map. The row origin has always been applied here - it is
|
||||||
|
// what makes the console's scrollback free - and the column origin has to be for the
|
||||||
|
// same reason, or text lands in the wrong cell the moment anything scrolls sideways.
|
||||||
|
//
|
||||||
|
// THE FINE OFFSETS ARE NOT APPLIED and must not be. They move the finished picture by
|
||||||
|
// less than a cell, and there is no such thing as less than a cell to write into.
|
||||||
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
const int mapRow = (scroll + screenRow) % VIDEO_MAP_ROWS;
|
||||||
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
uint8_t *cell = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE
|
||||||
+ column * VIDEO_CELL_BYTES;
|
+ mapColumn * VIDEO_CELL_BYTES;
|
||||||
cell[0] = tile;
|
cell[0] = tile;
|
||||||
cell[1] = attribute;
|
cell[1] = attribute;
|
||||||
}
|
}
|
||||||
@@ -161,6 +178,9 @@ void videoReset(void) {
|
|||||||
memset(videoRAM, 0, sizeof(videoRAM));
|
memset(videoRAM, 0, sizeof(videoRAM));
|
||||||
mode = VIDEO_MODE_40x25;
|
mode = VIDEO_MODE_40x25;
|
||||||
scroll = 0;
|
scroll = 0;
|
||||||
|
scrollColumn = 0;
|
||||||
|
fineX = 0;
|
||||||
|
fineY = 0;
|
||||||
renderedWidth = 0;
|
renderedWidth = 0;
|
||||||
renderedHeight = 0;
|
renderedHeight = 0;
|
||||||
lastFrame = videoNow;
|
lastFrame = videoNow;
|
||||||
@@ -202,6 +222,19 @@ uint8_t videoWrite(uint8_t value, uint8_t port) {
|
|||||||
// row that exists.
|
// row that exists.
|
||||||
scroll = (uint8_t)(value % VIDEO_MAP_ROWS);
|
scroll = (uint8_t)(value % VIDEO_MAP_ROWS);
|
||||||
break;
|
break;
|
||||||
|
case VIDEO_SCROLL_COLUMN:
|
||||||
|
// The same ring the other way. A map row is 256 bytes and a cell is two, so
|
||||||
|
// there are 128 columns whatever the mode shows.
|
||||||
|
scrollColumn = (uint8_t)(value % VIDEO_MAP_COLUMNS);
|
||||||
|
break;
|
||||||
|
case VIDEO_FINE_X:
|
||||||
|
// The low three bits and nothing else. Eight is not one cell along, it is zero
|
||||||
|
// again - see the note by the port numbers about why this does not carry.
|
||||||
|
fineX = (uint8_t)(value & VIDEO_FINE_MASK);
|
||||||
|
break;
|
||||||
|
case VIDEO_FINE_Y:
|
||||||
|
fineY = (uint8_t)(value & VIDEO_FINE_MASK);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
// Everything else is read only or not there yet. Writing does nothing rather
|
// Everything else is read only or not there yet. Writing does nothing rather
|
||||||
// than refusing: a port block reserved for later should be quiet, not fatal.
|
// than refusing: a port block reserved for later should be quiet, not fatal.
|
||||||
@@ -236,6 +269,9 @@ uint8_t videoRead(uint8_t port) {
|
|||||||
// wants one place to live.
|
// wants one place to live.
|
||||||
return 0;
|
return 0;
|
||||||
case VIDEO_MODE: return mode;
|
case VIDEO_MODE: return mode;
|
||||||
|
case VIDEO_SCROLL_COLUMN: return scrollColumn;
|
||||||
|
case VIDEO_FINE_X: return fineX;
|
||||||
|
case VIDEO_FINE_Y: return fineY;
|
||||||
// Asked rather than assumed. A program that wants to know how wide the screen is
|
// 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.
|
// should be able to find out, the same way it asks the console what mode it is in.
|
||||||
case VIDEO_COLUMNS: return (uint8_t)columnsFor(mode);
|
case VIDEO_COLUMNS: return (uint8_t)columnsFor(mode);
|
||||||
@@ -270,14 +306,24 @@ void videoRender(void) {
|
|||||||
const int rows = rowsFor(mode);
|
const int rows = rowsFor(mode);
|
||||||
const int width = columns * VIDEO_CELL_PIXELS;
|
const int width = columns * VIDEO_CELL_PIXELS;
|
||||||
|
|
||||||
for (int row = 0; row < rows; row++) {
|
const int height = rows * VIDEO_CELL_PIXELS;
|
||||||
|
|
||||||
|
// ---- One more row and one more column than fit ----
|
||||||
|
//
|
||||||
|
// With a fine offset the screen no longer starts on a cell boundary, so the first cell
|
||||||
|
// of each axis is partly above or left of the picture and one extra is needed at the far
|
||||||
|
// end to fill what that uncovered. Both are drawn and clipped, which is why every write
|
||||||
|
// below is guarded rather than trusted: the two edge cells are the only ones that can
|
||||||
|
// fall outside, but they fall outside on every frame that is not cell aligned.
|
||||||
|
for (int row = 0; row <= rows; row++) {
|
||||||
// The ring. Rows that scrolled off the top are still in the map, which is what
|
// The ring. Rows that scrolled off the top are still in the map, which is what
|
||||||
// makes scrollback free rather than something the console has to keep itself.
|
// makes scrollback free rather than something the console has to keep itself.
|
||||||
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
const int mapRow = (scroll + row) % VIDEO_MAP_ROWS;
|
||||||
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
const uint8_t *cells = videoRAM + VIDEO_MAP_BASE + mapRow * VIDEO_MAP_STRIDE;
|
||||||
for (int column = 0; column < columns; column++) {
|
for (int column = 0; column <= columns; column++) {
|
||||||
const uint8_t tile = cells[column * VIDEO_CELL_BYTES];
|
const int mapColumn = (scrollColumn + column) % VIDEO_MAP_COLUMNS;
|
||||||
uint8_t attribute = cells[column * VIDEO_CELL_BYTES + 1];
|
const uint8_t tile = cells[mapColumn * VIDEO_CELL_BYTES];
|
||||||
|
uint8_t attribute = cells[mapColumn * VIDEO_CELL_BYTES + 1];
|
||||||
// ---- The cursor, turned inside out ----
|
// ---- The cursor, turned inside out ----
|
||||||
//
|
//
|
||||||
// Not a glyph of its own, because a block drawn over a cell hides what is in it
|
// Not a glyph of its own, because a block drawn over a cell hides what is in it
|
||||||
@@ -301,18 +347,28 @@ void videoRender(void) {
|
|||||||
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
const uint8_t bank = (uint8_t)((attribute & 0x0F) << 4);
|
||||||
const uint8_t *art = videoRAM + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
const uint8_t *art = videoRAM + VIDEO_TILE_BASE + tile * VIDEO_TILE_BYTES;
|
||||||
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
for (int y = 0; y < VIDEO_CELL_PIXELS; y++) {
|
||||||
uint8_t *out = pixels + ((row * VIDEO_CELL_PIXELS + y) * width
|
// Where this row of the cell lands once the view has been slid up by the
|
||||||
+ column * VIDEO_CELL_PIXELS) * 3;
|
// fine offset. Negative means it is the part of the top cell that is off
|
||||||
|
// the screen, which is the whole point of drawing it.
|
||||||
|
const int atY = row * VIDEO_CELL_PIXELS + y - fineY;
|
||||||
|
if (atY < 0 || atY >= height) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
for (int x = 0; x < VIDEO_CELL_PIXELS; x++) {
|
||||||
|
const int atX = column * VIDEO_CELL_PIXELS + x - fineX;
|
||||||
|
if (atX < 0 || atX >= width) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
// Wrapping, because a byte plus a byte is a byte. A tile using the
|
// Wrapping, because a byte plus a byte is a byte. A tile using the
|
||||||
// high end of the palette with a nibble set comes round the bottom,
|
// high end of the palette with a nibble set comes round the bottom,
|
||||||
// which is what an adder does and what the manual says it does.
|
// which is what an adder does and what the manual says it does.
|
||||||
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
const uint8_t index = (uint8_t)(art[y * VIDEO_CELL_PIXELS + x] + bank);
|
||||||
const uint8_t *entry = videoRAM + VIDEO_PALETTE_BASE
|
const uint8_t *entry = videoRAM + VIDEO_PALETTE_BASE
|
||||||
+ index * VIDEO_PALETTE_BYTES;
|
+ index * VIDEO_PALETTE_BYTES;
|
||||||
*out++ = entry[0];
|
uint8_t *out = pixels + (atY * width + atX) * 3;
|
||||||
*out++ = entry[1];
|
out[0] = entry[0];
|
||||||
*out++ = entry[2];
|
out[1] = entry[1];
|
||||||
|
out[2] = entry[2];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,6 +108,25 @@ int videoTextRows(void);
|
|||||||
#define VIDEO_SCROLL 0x34
|
#define VIDEO_SCROLL 0x34
|
||||||
#define VIDEO_CONTROL 0x35
|
#define VIDEO_CONTROL 0x35
|
||||||
|
|
||||||
|
// ---- The other three quarters of scrolling ----
|
||||||
|
//
|
||||||
|
// 0x34 moves the view a whole cell at a time and only downwards, which is a scrolling text
|
||||||
|
// screen and not a scrolling picture. These are the rest of it: a column origin so the map
|
||||||
|
// can be wider than the screen as well as taller, and a pixel remainder for each axis so the
|
||||||
|
// step can be one pixel rather than eight.
|
||||||
|
//
|
||||||
|
// COARSE AND FINE DO NOT CARRY INTO EACH OTHER. Fine is the low three bits of what is
|
||||||
|
// written and nothing else, so a program that scrolls past a cell edge advances the coarse
|
||||||
|
// register itself. That is what the machines this one is pretending to be did, it keeps each
|
||||||
|
// register meaning exactly one thing, and it means a program always knows where it is
|
||||||
|
// without reading anything back off the screen.
|
||||||
|
#define VIDEO_SCROLL_COLUMN 0x36
|
||||||
|
#define VIDEO_FINE_X 0x37
|
||||||
|
#define VIDEO_FINE_Y 0x38
|
||||||
|
|
||||||
|
// Eight pixels to a cell, so three bits say where inside one the view begins.
|
||||||
|
#define VIDEO_FINE_MASK 0x07
|
||||||
|
|
||||||
// ---- The frame ----
|
// ---- The frame ----
|
||||||
//
|
//
|
||||||
// A screen finishes drawing sixty times a second and then has a moment before it starts
|
// A screen finishes drawing sixty times a second and then has a moment before it starts
|
||||||
|
|||||||
@@ -597,8 +597,11 @@ The high nibble is reserved and should be left at zero, so that a meaning can be
|
|||||||
| 0x31 | Mode. |
|
| 0x31 | Mode. |
|
||||||
| 0x32 | Columns, read only. |
|
| 0x32 | Columns, read only. |
|
||||||
| 0x33 | Rows, read only. |
|
| 0x33 | Rows, read only. |
|
||||||
| 0x34 | Scroll. |
|
| 0x34 | Scroll row. Which of the map's 128 rows is drawn at the top. |
|
||||||
| 0x35 | Control. Bit 0 asks to be interrupted at each frame. |
|
| 0x35 | Control. Bit 0 asks to be interrupted at each frame. |
|
||||||
|
| 0x36 | Scroll column. Which of the map's 128 columns is drawn at the left. |
|
||||||
|
| 0x37 | Fine X. How many pixels into that column the screen begins, 0 to 7. |
|
||||||
|
| 0x38 | Fine Y. How many pixels into that row the screen begins, 0 to 7. |
|
||||||
|
|
||||||
| Mode | Screen | Cells |
|
| Mode | Screen | Cells |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
@@ -648,6 +651,39 @@ Scrolling therefore moves a register and no memory at all. That is not a small s
|
|||||||
|
|
||||||
And the rows that scrolled off are still in the map, which is where a terminal on this machine gets scrollback without having to keep any.
|
And the rows that scrolled off are still in the map, which is where a terminal on this machine gets scrollback without having to keep any.
|
||||||
|
|
||||||
|
**The columns are the same ring the other way.** A map row is 256 bytes and a cell is two, so there are 128 of them whatever the mode shows - 88 more than a 40 column screen displays, and 48 more than an 80. Scroll column says which one is at the left, and screen column *c* shows map column *scroll column + c*, wrapped. A map wider than the screen costs nothing to have, because the map is that wide already.
|
||||||
|
|
||||||
|
### Scrolling By Less Than A Cell:
|
||||||
|
|
||||||
|
The two registers above move the view a whole cell at a time, which is a scrolling text screen rather than a scrolling picture: eight pixels is a long way to jump sixty times a second. **Fine X and Fine Y are the remainder** - how far into the cell at the origin the screen actually starts. Together the four registers place the view anywhere in the map to the pixel.
|
||||||
|
|
||||||
|
The screen no longer begins on a cell boundary when a fine register is not zero, so the cells at two edges are partly off it. That is the device's problem and not a program's: it draws one more row and one more column than fit and clips them.
|
||||||
|
|
||||||
|
**Fine does not carry into coarse.** Writing 8 to a fine register is writing 0, because only the low three bits of it mean anything - it is not one cell along. A program scrolling past a cell edge advances the coarse register itself:
|
||||||
|
|
||||||
|
```asm
|
||||||
|
; One pixel to the left, carrying when it runs out of cell.
|
||||||
|
SETD.0 FineX
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
INIB 0x07
|
||||||
|
AND
|
||||||
|
STQ.0
|
||||||
|
BNQ scrolled ; Still inside the cell.
|
||||||
|
SETD.0 CoarseX
|
||||||
|
LDA.0
|
||||||
|
INCA
|
||||||
|
STA.0
|
||||||
|
OUTA 0x36
|
||||||
|
scrolled:
|
||||||
|
```
|
||||||
|
|
||||||
|
The alternative was to let a write of 8 step the column and set the fine part to zero, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic.
|
||||||
|
|
||||||
|
**The fine registers move the picture and nothing else.** Writing a character still lands in a whole cell, because there is no such thing as less than a cell to write into - so a program may scroll to any pixel and the console's idea of where row three, column five is does not move underneath it. The coarse registers are the ones the console follows, and it has always followed the row.
|
||||||
|
|
||||||
|
**None of the four does anything in bitmap mode**, which has no map to slide.
|
||||||
|
|
||||||
### Writing On The Screen:
|
### Writing On The Screen:
|
||||||
|
|
||||||
A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both.
|
A console on a machine with a screen sends every byte to both, because a machine with a screen and a serial line is an ordinary machine and there is one console driving both.
|
||||||
|
|||||||
+9
-1
@@ -510,7 +510,15 @@ else:
|
|||||||
# The minimal application in the same section is what somebody copies, so it is the
|
# The minimal application in the same section is what somebody copies, so it is the
|
||||||
# part of the map most worth being right. It went stale across the doubling while the
|
# part of the map most worth being right. It went stale across the doubling while the
|
||||||
# table above it was corrected.
|
# table above it was corrected.
|
||||||
example = re.search(r"```asm\n(.*?)```", readme, re.S)
|
# ---- Found by its section, not by being first ----
|
||||||
|
#
|
||||||
|
# This took the first asm block in the file, which was the minimal application right up
|
||||||
|
# until somebody documented a program with an assembly example above it - and then this
|
||||||
|
# said the minimal application had no #Base, about a block that was never claiming to be
|
||||||
|
# one. The example lives under System Services; that is what identifies it.
|
||||||
|
services = readme.split("### System Services:", 1)
|
||||||
|
example = (re.search(r"```asm\n(.*?)```", services[1], re.S)
|
||||||
|
if len(services) > 1 else None)
|
||||||
if not example:
|
if not example:
|
||||||
problems.append("the CosmOS README no longer shows a minimal application")
|
problems.append("the CosmOS README no longer shows a minimal application")
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 510
|
Grid.sbx 539
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
hi.script 121
|
hi.script 121
|
||||||
bad.script 45
|
bad.script 45
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Snake.sbx 2164
|
|||||||
Keys.sbx 664
|
Keys.sbx 664
|
||||||
Say.sbx 156
|
Say.sbx 156
|
||||||
Break.sbx 149
|
Break.sbx 149
|
||||||
Grid.sbx 510
|
Grid.sbx 539
|
||||||
notes.txt 21
|
notes.txt 21
|
||||||
hi.script 121
|
hi.script 121
|
||||||
bad.script 45
|
bad.script 45
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Two questions video.sh cannot ask of a picture that moves.
|
||||||
|
#
|
||||||
|
# Grid scrolls a pixel a frame, so nothing can be asked about a particular pixel: whether
|
||||||
|
# pixel 4 is a grid line depends on which frame this is. Both questions below are true of the
|
||||||
|
# picture whatever the fine scroll offset happens to be.
|
||||||
|
#
|
||||||
|
# grid - is this a grid of one repeated tile? Periodic every eight pixels across, and
|
||||||
|
# not all one colour.
|
||||||
|
# bands - does the attribute nibble recolour each cell row? The colours found in one band
|
||||||
|
# of eight rows differ from those in the next, whatever the ground is doing.
|
||||||
|
import sys
|
||||||
|
|
||||||
|
data = open(sys.argv[1], "rb").read()
|
||||||
|
fields = data.split(b"\n", 3)
|
||||||
|
width = int(fields[1].split()[0])
|
||||||
|
body = fields[3]
|
||||||
|
|
||||||
|
|
||||||
|
def pixel(x, y):
|
||||||
|
at = (y * width + x) * 3
|
||||||
|
return tuple(body[at:at + 3])
|
||||||
|
|
||||||
|
|
||||||
|
if sys.argv[2] == "grid":
|
||||||
|
row = [pixel(x, 4) for x in range(32)]
|
||||||
|
periodic = all(row[x] == row[x + 8] for x in range(24))
|
||||||
|
varied = len(set(row)) > 1
|
||||||
|
print("yes" if periodic and varied else
|
||||||
|
"not periodic" if not periodic else "all one colour")
|
||||||
|
else:
|
||||||
|
# A band is eight rows, which is one cell row wherever the boundary has slid to. The
|
||||||
|
# ground is the same under every scheme, so what tells them apart is the line colour -
|
||||||
|
# and looking at the whole band finds it without knowing where in the band it is.
|
||||||
|
first = set(pixel(0, y) for y in range(8))
|
||||||
|
second = set(pixel(0, y) for y in range(8, 16))
|
||||||
|
print("yes" if first != second else "the same in both: %s" % sorted(first))
|
||||||
+82
-12
@@ -718,6 +718,71 @@ TOTAL="$(grep -oE 'after [0-9]+' "$BUILD/poller.out" | grep -oE '[0-9]+')"
|
|||||||
&& result ok "and the flag comes down when looked at" "$TOTAL cycles, so three frames passed" \
|
&& 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"
|
|| result no "and the flag comes down when looked at" "$TOTAL cycles, too few to be three frames"
|
||||||
|
|
||||||
|
# ---- Scrolling by less than a cell, and sideways ----
|
||||||
|
#
|
||||||
|
# A red tile in the corner and nowhere else, so that where it lands says exactly what the
|
||||||
|
# scroll registers did. Every check below is the SAME program with one register changed, and
|
||||||
|
# what is compared is where the red stops.
|
||||||
|
scrollSetup() {
|
||||||
|
prologue
|
||||||
|
poke 0xFC04 0xFF; poke 0xFC05 0x00; poke 0xFC06 0x00
|
||||||
|
for i in $(seq 0 63); do poke $((0x0040 + i)) 0x01; done
|
||||||
|
poke 0x4000 0x01; poke 0x4001 0x00
|
||||||
|
}
|
||||||
|
|
||||||
|
# Where it is with nothing scrolled: the red runs from 0 to 7 and stops.
|
||||||
|
{ scrollSetup; epilogue; } | run scroll0 || exit 1
|
||||||
|
[ "$(pixel scroll0 7 0)" = "255,0,0" ] && [ "$(pixel scroll0 8 0)" != "255,0,0" ] \
|
||||||
|
&& result ok "the tile ends at the cell edge" "red from 0 to 7" \
|
||||||
|
|| result no "the tile ends at the cell edge" "7 is $(pixel scroll0 7 0), 8 is $(pixel scroll0 8 0)"
|
||||||
|
|
||||||
|
# One pixel of fine X moves the picture one pixel LEFT: the view slides right, so the red
|
||||||
|
# now ends at 6. One pixel, not eight, is the whole point of the register.
|
||||||
|
{ scrollSetup; port 0x37 0x01; epilogue; } | run scrollfx || exit 1
|
||||||
|
[ "$(pixel scrollfx 6 0)" = "255,0,0" ] && [ "$(pixel scrollfx 7 0)" != "255,0,0" ] \
|
||||||
|
&& result ok "fine X moves it one pixel" "the edge went from 7 to 6" \
|
||||||
|
|| result no "fine X moves it one pixel" "6 is $(pixel scrollfx 6 0), 7 is $(pixel scrollfx 7 0)"
|
||||||
|
|
||||||
|
{ scrollSetup; port 0x38 0x01; epilogue; } | run scrollfy || exit 1
|
||||||
|
[ "$(pixel scrollfy 0 6)" = "255,0,0" ] && [ "$(pixel scrollfy 0 7)" != "255,0,0" ] \
|
||||||
|
&& result ok "and fine Y moves it one pixel" "the edge went from 7 to 6" \
|
||||||
|
|| result no "and fine Y moves it one pixel" "6 is $(pixel scrollfy 0 6), 7 is $(pixel scrollfy 0 7)"
|
||||||
|
|
||||||
|
# Seven is as far as it goes. Eight is zero again and NOT one cell along, which is what "it
|
||||||
|
# does not carry" means where a program can see it.
|
||||||
|
{ scrollSetup; port 0x37 0x08; epilogue; } | run scrollwrap || exit 1
|
||||||
|
[ "$(pixel scrollwrap 7 0)" = "255,0,0" ] && [ "$(pixel scrollwrap 8 0)" != "255,0,0" ] \
|
||||||
|
&& result ok "eight of fine is none of it" "the low three bits, and no carry" \
|
||||||
|
|| result no "eight of fine is none of it" "7 is $(pixel scrollwrap 7 0)"
|
||||||
|
|
||||||
|
# Coarse X moves a whole cell. With the column origin at 1 the corner cell is off the left
|
||||||
|
# and cell 1 of the map is where the screen starts - so the corner is no longer red.
|
||||||
|
{ scrollSetup; poke $((0x4000 + 2)) 0x01; port 0x36 0x01; epilogue; } | run scrollcx || exit 1
|
||||||
|
[ "$(pixel scrollcx 0 0)" = "255,0,0" ] && [ "$(pixel scrollcx 8 0)" != "255,0,0" ] \
|
||||||
|
&& result ok "coarse X moves a whole cell" "the map moved one cell left" \
|
||||||
|
|| result no "coarse X moves a whole cell" "0 is $(pixel scrollcx 0 0), 8 is $(pixel scrollcx 8 0)"
|
||||||
|
|
||||||
|
# And it is a ring, the same as the rows are. Column 127 is the last one a map row has, so
|
||||||
|
# an origin there puts it on screen with column 0 beside it.
|
||||||
|
{ scrollSetup; poke $((0x4000 + 127 * 2)) 0x01; port 0x36 0x7F; epilogue; } | run scrollwrapx || exit 1
|
||||||
|
[ "$(pixel scrollwrapx 0 0)" = "255,0,0" ] && [ "$(pixel scrollwrapx 8 0)" = "255,0,0" ] \
|
||||||
|
&& result ok "the columns are a ring too" "127 on screen with 0 beside it" \
|
||||||
|
|| result no "the columns are a ring too" "0 is $(pixel scrollwrapx 0 0), 8 is $(pixel scrollwrapx 8 0)"
|
||||||
|
|
||||||
|
# ---- And the console follows the column origin ----
|
||||||
|
#
|
||||||
|
# It has always followed the row origin, which is where its scrollback comes from. A letter
|
||||||
|
# written while the view is scrolled sideways has to land where the writer meant - on the
|
||||||
|
# screen - and not at the map cell that happens to share its number.
|
||||||
|
{ printf '#Program\nstart:\n'
|
||||||
|
port 0x36 0x03
|
||||||
|
say "A"
|
||||||
|
epilogue
|
||||||
|
} | run scrollconsole || exit 1
|
||||||
|
inked scrollconsole 2 1 \
|
||||||
|
&& result ok "the console writes where it means to" "the letter is in the first cell of the screen" \
|
||||||
|
|| result no "the console writes where it means to" "nothing at 2,1"
|
||||||
|
|
||||||
# ---- The tile engine, driven by a program rather than by the console ----
|
# ---- The tile engine, driven by a program rather than by the console ----
|
||||||
#
|
#
|
||||||
# Everything above drives the screen from a bare test program. This boots the whole system
|
# Everything above drives the screen from a bare test program. This boots the whole system
|
||||||
@@ -736,20 +801,25 @@ timeout 30 "$EMU" --fast --cycles 8000000 --keyboard "$BUILD/grid.keys" \
|
|||||||
"$BUILD/cosmos.bin" > "$BUILD/grid.out" 2>&1 || true
|
"$BUILD/cosmos.bin" > "$BUILD/grid.out" 2>&1 || true
|
||||||
|
|
||||||
if [ -f "$BUILD/grid.ppm" ]; then
|
if [ -f "$BUILD/grid.ppm" ]; then
|
||||||
# A cell is eight by eight with a line along its top and down its left, so within one
|
# ---- Asked in a way that a moving picture can answer ----
|
||||||
# cell the corner is line and the middle is ground - and the cell to the right starts
|
#
|
||||||
# with a line again. That is a grid rather than a wash of colour.
|
# Not "is pixel 0 a line and pixel 4 the ground", which was the first version and was
|
||||||
corner="$(pixel grid 0 0)"; middle="$(pixel grid 4 4)"; nextcell="$(pixel grid 8 4)"
|
# really a check that the scroll happened to be at a cell boundary. Grid now moves a pixel
|
||||||
[ "$corner" != "$middle" ] && [ "$nextcell" = "$corner" ] \
|
# a frame, so where the lines are depends on which frame this is - but a grid of one tile
|
||||||
&& result ok "a program drew a grid of its own tile" "line $corner, ground $middle" \
|
# is PERIODIC whatever the offset: every pixel matches the one eight along. And it is not
|
||||||
|| result no "a program drew a grid of its own tile" "corner $corner middle $middle next $nextcell"
|
# all one colour, or a blank screen would pass.
|
||||||
|
GRIDLIKE="$(python3 "$ROOT/Tests/periodic.py" "$BUILD/grid.ppm" grid)"
|
||||||
|
[ "$GRIDLIKE" = "yes" ] \
|
||||||
|
&& result ok "a program drew a grid of its own tile" "the picture repeats every eight pixels" \
|
||||||
|
|| result no "a program drew a grid of its own tile" "not a grid of one tile ($GRIDLIKE)"
|
||||||
|
|
||||||
# The attribute nibble adds sixteen to every index in the tile, so consecutive map rows
|
# The attribute nibble adds sixteen to every index in the tile, so consecutive map rows
|
||||||
# come out in consecutive schemes. Two cell rows apart must not be the same colour.
|
# come out in consecutive schemes. Eight pixels apart is one cell row apart whatever the
|
||||||
one="$(pixel grid 0 0)"; two="$(pixel grid 0 8)"
|
# fine offset is, so this one survives the scrolling too.
|
||||||
[ "$one" != "$two" ] \
|
BANDED="$(python3 "$ROOT/Tests/periodic.py" "$BUILD/grid.ppm" bands)"
|
||||||
&& result ok "the attribute nibble recolours it" "row 0 $one, row 1 $two" \
|
[ "$BANDED" = "yes" ] \
|
||||||
|| result no "the attribute nibble recolours it" "both rows are $one"
|
&& result ok "the attribute nibble recolours it" "each cell row is its own scheme" \
|
||||||
|
|| result no "the attribute nibble recolours it" "$BANDED"
|
||||||
else
|
else
|
||||||
result no "a program drew a grid of its own tile" "no picture came out"
|
result no "a program drew a grid of its own tile" "no picture came out"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user