add ID docs to src/docs and search index, F1 for Help window

This commit is contained in:
Steven Hugg committed 2026-09-12 14:43:28 +02:00
1 parent 17c08bbc85
commit 3df69e7d04
37 files changed
+1956 -1613

No files matched your search

+78
View File
@@ -479,6 +479,84 @@ div.markdown td {
div.markdown th {
padding:0.2em;
}
/* help window (#help/<topic>): Markdown docs shipped with the IDE */
.help-view {
background-color:#333;
color:#ddd;
padding:1.25em 1.5em;
font-size:1em;
line-height:1.5;
min-height:100%;
box-sizing:border-box;
font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
}
.help-view h1 {
color:#fff;
font-size:1.5em;
margin:0 0 0.6em 0;
}
.help-view h2 {
color:#7ec7ff;
font-size:1.25em;
margin:1.4em 0 0.5em 0;
padding-bottom:0.2em;
border-bottom:1px solid #444;
}
.help-view h3 {
color:#9fd0ff;
font-size:1.05em;
margin:1.1em 0 0.4em 0;
}
.help-view a {
color:#7ea6ff;
}
.help-view a:hover {
color:#a9c6ff;
}
.help-view code {
background:#1d1d1d;
color:#ffd479;
padding:0.1em 0.3em;
border-radius:3px;
font-size:0.9em;
}
.help-view pre {
background:#1d1d1d;
padding:0.75em 1em;
border-radius:4px;
overflow-x:auto;
}
.help-view pre code {
background:none;
padding:0;
color:#e8e8e8;
}
.help-view table {
border-collapse:collapse;
margin:1em 0;
}
.help-view th, .help-view td {
border:1px solid #444;
padding:0.3em 0.6em;
text-align:left;
}
.help-view th {
background:#383838;
}
.help-view blockquote {
border-left:3px solid #555;
margin:1em 0;
padding:0.2em 1em;
color:#bbb;
}
.help-view hr {
border:none;
border-top:1px solid #444;
margin:1.5em 0;
}
.help-view img {
max-width:100%;
}
#sidebar {
overflow:auto;
}
-414
View File
@@ -1,414 +0,0 @@
# Asset Editor Graphics Format
The 8bitworkshop IDE scans your source files for **asset headers** — small JSON
descriptors in comments — and turns the data block that follows into an editable
bitmap, tilemap, or palette. Edits are written straight back into the source text,
preserving the original number base and formatting of each literal.
Source of truth:
| What | Where |
| --- | --- |
| Header scanning, decode/encode, palettes | `src/ide/pixeleditor.ts` |
| Editor pipeline & view construction | `src/ide/views/asseteditor.ts` |
| In-app reference dialog (Help ▸ Asset Editor) | `openAssetEditorHelp()` in `src/ide/ui.ts` |
---
## 1. Header syntax
A header is a comment containing a single JSON object, immediately followed by a
data block. The data block ends at the next closing delimiter:
| Language | Header | Data block ends at |
| --- | --- | --- |
| C / C++ | `/*{w:8,h:8}*/` | next `;` |
| Assembly | `;;{w:8,h:8};;` | next `;;` |
| Verilog | `/*{w:8,h:8}*/` | next `end` |
The Verilog terminator is selected by platform (`platform_id.includes('verilog')`),
not by file extension.
**Literals must carry a radix prefix.** Recognized: `0xNN`, `$NN`, `#$NN`, `%0101`,
`0b0101`, `8'hNN`, `8'b0101`, and assembler `hex 1f20…` statements. Each is
rewritten in its original notation when you edit, so a file of `$`-prefixed bytes
stays `$`-prefixed. **Plain decimal is not matched at all** — a block of
`{24,60,126,…}` parses as zero values and reports *"Expected 8 value(s), found 0"*.
```c
/*{w:8,h:8,bpp:1,count:2,brev:1}*/
const byte tiles[] = {
0xff,0x81,0x81,0x81,0x81,0x81,0x81,0xff,
0x18,0x3c,0x7e,0xff,0xff,0x7e,0x3c,0x18,
};
```
```asm
;;{w:8,h:8,count:1,brev:1,flip:1};;
PlayerGfx:
.byte $18,$3c,$7e,$ff,$ff,$7e,$3c,$18
;;
```
### JSON leniency
Keys are unquoted — the scanner rewrites `([A-Za-z]+):` to `"$1":` before calling
`JSON.parse`. String *values* still need quotes: `pal:"nes"`, `comp:"rletag"`.
### Gotchas
* **The first `;` wins.** In C the block ends at the very next semicolon, so the
array declaration must be the only statement between the header and that `;`.
* Keys are case-sensitive and must be alphabetic.
* A malformed header is reported inline in the Asset Editor tab rather than
silently ignored.
### External data with `#embed`
If the data block contains a C23 `#embed` directive, the bytes are read from that
file instead of from the source text:
```c
/*{w:24,h:21,bpp:1,brev:1,wpimg:64,count:1}*/
const char sprite[] = {
#embed "sprite.bin"
};
```
The path is resolved first as given, then relative to the including file's
directory (`sub/main.c` + `data.bin` → `sub/data.bin`). The byte length of the
file is validated against the format the same way inline data is.
---
## 2. The address model
Everything except the special modes in §6 is described by one addressing formula.
Understanding it is the difference between guessing at fields and deriving them.
```
bpw = bpw ?? 8 // bits per word
sl = sl ?? ceil(w * bpp / bpw) // words per scanline
wpimg = wpimg ?? sl * h // words per image
pofs = pofs ?? sl * h * count // distance between bitplanes
rowstride = sl // (see il: below)
for each image n, for each row y:
yp = flip ? h-1-y : y
ofs0 = n*wpimg + yp*rowstride
shift = 0
for each column x:
ofs = remap(ofs0) // bit permutation, §4
for each plane p:
word = data[ofs + p*pofs + skip]
bits = brev ? word >> (bpw-shift-bpp) : word >> shift
color |= (bits & ((1<<bpp)-1)) << (p*bpp)
shift += bpp
if shift >= bpw: // move to next word in the row
ofs0 += 1
shift = 0
```
Four consequences worth internalizing:
1. **`remap` is applied to `ofs0` inside the `x` loop**, after the byte-advance.
It therefore sees the byte index *within* a row as well as the row and image
index — which is exactly what lets you describe multi-tile-wide sprites (§4).
2. **`skip` is added last**, after `remap` and after the plane offset. It offsets
the whole block, not each image.
3. **`pofs` defaults to "one plane block after another"** (`sl*h*count`). Set it
explicitly for anything interleaved.
4. Total colors per pixel = `1 << (bpp * np)`.
### Bit order
`brev:1` means **MSB is the leftmost pixel** — the usual arrangement for NES,
Game Boy, TMS9918, VCS, and most tile-based hardware. Without it, LSB is leftmost
(Apple II HGR, some bitmap formats).
### Planes vs. bits-per-pixel
Two ways to get more than two colors, and most formats use one or the other:
* **`bpp:N`** — the bits of one pixel are adjacent within a word. `bpp:4` packs
two 16-color pixels per byte.
* **`np:N`** — the bits of one pixel live in N separate words, `pofs` apart.
Plane `p` supplies bits `p*bpp`.
Game Boy 2bpp is `bpp:1,np:2,pofs:1,sl:2` — planes interleaved every other byte.
NES 2bpp is `bpp:1,np:2,pofs:8` — plane 0 in bytes 0–7, plane 1 in bytes 8–15.
### `il` — row interleaving
`il:1` stores all `count` images as one wide block, row by row:
`wpimg` becomes `sl`, and `rowstride` becomes `sl*count`. Useful for character
sets stored as a single wide strip.
---
## 3. Field reference
### Image fields
| Field | Default | Description |
| --- | --- | --- |
| `w` | *required* | Width in pixels |
| `h` | *required* | Height in pixels |
| `count` | `1` | Number of images |
| `bpp` | `1` | Bits per pixel |
| `np` | `1` | Number of bitplanes (colors = 2<sup>bpp×np</sup>) |
| `bpw` | `8` | Bits per word (8, 16, 32) |
| `sl` | `ceil(w*bpp/bpw)` | Words per scanline (stride) |
| `wpimg` | `sl*h` | Words per image — use when hardware pads images |
| `pofs` | `sl*h*count` | Distance between bitplanes, in words |
| `skip` | `0` | Words to skip at the start of the whole block |
| `brev` | `false` | MSB is the leftmost pixel |
| `flip` | `false` | Data is stored bottom row first |
| `il` | `false` | Interleave images row by row |
| `remap` | — | Bit permutation applied to the word offset (§4) |
| `reindex` | — | Per-column word/bit table (§5) |
| `aspect` | `1` | Pixel aspect ratio for display only |
| `art` | `false` | Apple II HGR artifact-color mode (§6) |
| `comp` | — | `"rletag"` = RLE-compressed block (§6) |
| `map` | — | `"nesnt"` = NES nametable, not a bitmap (§6) |
| `pacstrip` | — | Pac-Man/Namco vertical-strip layout (§6) |
| `xform` | — | CSS transform. Parsed and stored, but currently inert: the editor overwrites it with `scale(2)` and the line that would apply it is commented out |
### Palette fields
A block with `pal` and no `w`/`h` becomes a palette editor.
| Field | Default | Description |
| --- | --- | --- |
| `pal` | — | Palette decoding, see below |
| `n` | — | Advisory entry count — the real count comes from the data |
| `layout` | — | Grouped editor layout: `nes`, `astrocade`, `pacman` |
`pal` is either:
* **A number** like `332` or `444` — the three digits are channel bit widths,
applied **from the low bits upward**. A positive value puts red lowest
(`pal:332` = RGB332, red in bits 0–2); a **negative** value puts blue lowest
(`pal:-332` = blue in bits 0–2, red in the top 2 bits). Note the picker's color
space is `1 << (sum of digits)` entries, which is why `pal:444` is a 4096-color
chooser regardless of how many entries the block holds.
* **A name**: `"nes"`, `"vcs"`, `"c64"`, `"gb"`, `"ap2lores"`, `"astrocade"`, or
`"pacman"` (Namco color-PROM bytes, decoded with MAME's resistor weights).
These are the only recognized names — an unknown one throws
`No palette named X`, which the Asset Editor reports in place of *every* asset
in that file. (The `pal:"pce"` blocks in `presets/pce/*_gfx.h` currently hit
this; there is no `pce` entry in `PREDEF_PALETTES`.)
Bitmaps pick up their colors from palette blocks **anywhere in the open project**,
matched by entry count — a 4-color bitmap offers every 4-entry palette (and every
4-entry slice named by a `layout`) in a dropdown. Game Boy projects fall back to
the DMG green scale when no palette block matches.
```c
/*{pal:"nes",layout:"nes"}*/
const char PALETTE[8] = { 0x0F, 0x11,0x24,0x3C, 0x00, 0x01,0x15,0x25 };
```
`bpw` applies to palette blocks too — PC Engine palettes are 16-bit words, so
their blocks carry `bpw:16` for correct parsing and write-back.
---
## 4. `remap` — describing hardware tile layouts
`remap` permutes the **bits of the word offset**. Entry `i` of the array says
which destination bit source bit `i` becomes:
```
remap(ofs) = OR over i of: bit i of ofs → bit remap[i] of result
```
A **negative** entry `-n` maps source bit `i` to destination bit `n-1` **and
inverts it** — use it when the hardware stores the halves in the opposite order.
This exists because the natural row-major offset (`n*wpimg + y*sl + xbyte`) rarely
matches how hardware stores multi-tile sprites. Writing the two offsets side by
side in binary tells you the array.
### Worked example: NES 16×16 metasprite
From `presets/nes/shoot2.c`:
```c
/*{w:16,h:16,bpp:1,count:16,brev:1,np:2,pofs:8,remap:[5,0,1,2,4,6,7,8,9,10,11,12]}*/
```
`sl` = 2, `wpimg` = 32, so the natural offset is `32n + 2y + xb`:
| Source bit | Meaning | → | Dest bit | Contributes |
| --- | --- | --- | --- | --- |
| 0 | `xb` (left/right byte) | → | 5 | 32 |
| 1,2,3 | `y0,y1,y2` | → | 0,1,2 | `y & 7` |
| 4 | `y3` (top/bottom half) | → | 4 | 16 |
| 5+ | image index `n` | → | 6+ | 64·n |
Physical offset = `64n + 32·xb + 16·y3 + (y&7)`, plus `p*8` for the plane. That is
exactly NES CHR: four 16-byte tiles per metasprite in 8×16 sprite order —
upper-left, lower-left, upper-right, lower-right.
### Worked example: Game Boy 16×16 metasprite
From `presets/gb/pakupaku.c`:
```c
/*{w:16,h:16,bpp:1,count:19,brev:1,np:2,pofs:1,sl:2,wpimg:64,remap:[5,1,2,3,4,0,6,7,8,9,10,11,12]}*/
```
Game Boy interleaves its planes (`pofs:1`), so `sl:2` is already consumed by the
two planes of a single 8-pixel row, and the natural offset `64n + 2y + xb` puts
`xb` in bit 0 and `y` in bits 1–4. Bit 5 is never set. The remap sends bit 0 (the
8×16 column) up to bit 5 and folds the unused bit 5 down to 0, producing
`64n + 32·xb + 16·y3 + 2·(y&7)` — the L-top, L-bot, R-top, R-bot tile order the
game's OAM code uses.
### `remap` must be a bijection
The size check computes the required data length as
`max over n,i of remap(n*wpimg + i)` for `i` in `[0, wpimg)`, plus one. If your
remap isn't a permutation of that range, the maximum comes out wrong and you get a
spurious *"Expected N values, found M"* error. This is why the Game Boy example
above bothers to map the unused bit 5 down to bit 0 instead of leaving it alone —
the mapping is never exercised during decoding, but it keeps the range exact.
### Other patterns in the tree
| Format | Meaning |
| --- | --- |
| `remap:[0,1,2,4,5,6,7,8,9,10,11,12]` | NES 8×8: doubles the image stride to 16 bytes so the two 8-byte planes fit |
| `remap:[4,0,1,2,3,5,6,7,8,9]` | TMS9918 16×16 sprite: UL, LL, UR, LR (ColecoVision, MSX) |
| `remap:[-5,0,1,2,3,5,6,7,8,9]` | Same, with the halves stored right-first |
| `remap:[3,0,1,2,4,5,6,7,8,9,10]` | Galaxian/Scramble 16×16 sprite |
---
## 5. `reindex`
`reindex` replaces the shift/advance logic entirely with a per-column table. For
column `x`, `reindex[x % len]` is a **bit index within the row**: `>> 3` gives the
word offset added to the row base, `& 7` gives the bit position (still subject to
`brev`). Entry `15` therefore means word 1, bit 7. Because it drives the offset
directly, the automatic `ofs0 += 1` advance is disabled.
It exists for hardware where pixel order within a row is not linear — notably VCS
playfields, where the three playfield registers are not all in the same bit order:
```asm
;;{w:20,h:10,flip:1,reindex:[4,5,6,7,15,14,13,12,11,10,9,8,16,17,18,19]};;
```
---
## 6. Special modes
### `comp:"rletag"`
The block is RLE-compressed: the first byte is the tag, a literal byte sets the
current value, and a tag byte is followed by a repeat count (count 0 = end).
Decompression happens before decoding; the editor shows the expanded image.
### `map:"nesnt"`
The block is an NES nametable, not a bitmap. `w`/`h` are in **tiles** (default
32×30) and the block is rendered as a tilemap using the project's CHR data, with a
map editor instead of a pixel editor. Usually paired with `comp:"rletag"`:
```c
/*{w:32,h:30,bpp:8,comp:"rletag",map:"nesnt"}*/
```
### `pacstrip:1`
Pac-Man / Namco arcade layout: 8-byte vertical strips of 4 rows × 8 columns,
2bpp packed as bit `y` plus bit `y+4`, X/Y mirrored within each strip. Tiles (8×8)
use 2 strips; sprites (16×16) use 8 strips in hardware order. The mode implies
4 colors regardless of `bpp`.
```c
/*{w:8,h:8,count:256,bpp:2,pacstrip:1}*/
/*{w:16,h:16,count:64,bpp:2,pacstrip:1}*/
```
The strip tables must stay in sync with `PacmanVideo` in `src/machine/pacman.ts`.
### `art:1`
Apple II HGR artifact color: bit 7 of each byte selects the color set, and the
editor shows a per-group palette toggle. The artifact bit is bit 0 when `brev` is
set, bit 7 otherwise. No preset currently uses it — the Apple II presets declare
plain `bpp:1` bitmaps.
---
## 7. Validation
Before creating an editor, the number of parsed words (= bytes when `bpw` is 8) is
checked against the format:
```
required = max(remap(n*wpimg + i)) + planeExtent + 1 + skip
planeExtent = (np-1)*pofs, or 0 if that is smaller than wpimg
```
The `planeExtent` rule distinguishes the two plane layouts: when planes are
interleaved (`pofs < wpimg`, Game Boy/SMS) the plane bytes are already inside each
image block; when planes are stored as separate blocks (`pofs >= wpimg`, NES) the
extra plane is counted at the end.
A mismatch shows as **"Expected N value(s), found M"** in the Asset Editor tab.
When that happens, check in this order:
1. **`found 0`** — the literals have no radix prefix (§1), or the data block ended
early at a stray `;`.
2. `count` — the most common culprit otherwise.
3. `wpimg` — hardware often pads (C64 sprites are 24×21 bits in 64 bytes).
4. `remap` — is it a bijection over `[0, wpimg)`? (§4)
5. `pofs` — interleaved planes need it set explicitly.
---
## 8. Cookbook
Real formats from the preset tree:
| Platform | Header |
| --- | --- |
| NES 8×8 CHR | `/*{w:8,h:8,bpp:1,count:256,brev:1,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}*/` |
| NES 16×16 metasprite | `/*{w:16,h:16,bpp:1,count:16,brev:1,np:2,pofs:8,remap:[5,0,1,2,4,6,7,8,9,10,11,12]}*/` |
| NES palette | `/*{pal:"nes",layout:"nes"}*/` |
| NES nametable | `/*{w:32,h:30,bpp:8,comp:"rletag",map:"nesnt"}*/` |
| Game Boy 8×8 tile | `/*{w:8,h:8,bpp:1,count:4,brev:1,np:2,pofs:1,sl:2}*/` |
| Game Boy 16×16 metasprite | `/*{w:16,h:16,bpp:1,count:19,brev:1,np:2,pofs:1,sl:2,wpimg:64,remap:[5,1,2,3,4,0,6,7,8,9,10,11,12]}*/` |
| ColecoVision / MSX sprite | `/*{w:16,h:16,brev:1,remap:[4,0,1,2,3,5,6,7,8,9],count:2}*/` |
| ColecoVision layered 2-plane sprite | `/*{w:16,h:16,remap:[-5,0,1,2,3,5,6,7,8,9],count:5,np:2}*/` |
| C64 hires sprite | `/*{w:24,h:21,bpp:1,brev:1,wpimg:64,aspect:1,count:3}*/` |
| C64 multicolor sprite | `/*{w:12,h:21,bpp:2,brev:1,wpimg:64,count:4,aspect:2}*/` |
| VCS sprite | `;;{w:8,h:16,brev:1,flip:1};;` |
| VCS palette | `;;{pal:"vcs"};;` |
| Galaxian/Scramble tile ROM | `/*{w:16,h:16,remap:[3,0,1,2,4,5,6,7,8,9,10],brev:1,np:2,pofs:2048,count:64}*/` |
| Pac-Man tiles / sprites | `/*{w:8,h:8,count:256,bpp:2,pacstrip:1}*/` · `/*{w:16,h:16,count:64,bpp:2,pacstrip:1}*/` |
| Pac-Man color PROM | `/*{pal:"pacman",n:32}*/` |
| Astrocade | `/*{w:16,h:16,bpp:2,brev:1}*/` · `/*{pal:"astrocade",layout:"astrocade"}*/` |
| Williams | `/*{w:16,h:16,bpp:4,brev:1}*/` |
| PC Engine 16×16 sprite | `/*{w:16,h:16,bpp:1,count:3,brev:1,np:4,pofs:16,sl:1,bpw:16,wpimg:64}*/` |
| Apple II HGR | `/*{w:8,h:8,bpp:1,count:96}*/` (LSB-first, no `brev`) |
| Verilog 16-bit words | `/*{w:16,h:16,bpw:16,count:5}*/` |
---
## 9. Deriving a new format
1. Write down how the hardware stores one image: bytes per row, where the second
plane lives, whether rows go top-down.
2. Set `w`, `h`, `bpp`, `np`, `brev`, `flip` from that.
3. Compute the natural offset `n*wpimg + y*sl + xbyte` and the physical offset the
hardware actually uses. If they differ, write both in binary and read off
`remap` — source bit position on the left, destination on the right.
4. Confirm `count * wpimg` equals the real byte count; adjust `wpimg` if the
hardware pads.
5. Open the Asset Editor tab. Wrong `brev` looks mirrored; wrong `remap` looks
like scrambled or swapped quadrants; wrong `pofs` shows one plane as noise.
-91
View File
@@ -1,91 +0,0 @@
C COMPILERS
cc65 - A fairly mature C compiler + toolchain for the 6502 family with lots of target
platforms and an extensive library. The code generation is
not great, using the AX register pair and a parameter stack for local variables that has to be
indirect-indexed (you can make locals static with an option, but I found
this introduced compiler bugs.)
LCC - A small retargetable C compiler released in 1994, most notoriously
used to compile bytecode for the QuakeC language. It doesn't generate 6502
code, but it probably could -- although optimizations are weak.
There's an older port to the 65C816.
ACK - This ancient compiler used for Minix has a lot of frontends and backends, and is designed
for small-register CISC CPUs. There's a 6500 backend but I couldn't get it
working. Looks like it could be resurrected though. There's an intermediate
file for each step of the pipeline.
PCC - An early C compiler from the days of UNIX, now updated to C99.
There's a 16-bit target (x86) and the backends looks pretty simple.
http://pcc.ludd.ltu.se/internals/pass2/
SDCC - A mature compiler that supports lots of microcontrollers including
the Z80. This is probably the best candidate for porting to 6502,
but the backends are bespoke works of evil genius and there's precious little documentation.
(I wonder if you could post-process the HC08 output to 6502, since they're similar in some ways.)
LLVM-6502 - Someone uploaded this to Github. I got it to build, but the code
generated wasn't comprehensible.
osXdk - A fork of the Oric SDK. The C compiler has a weird design which
emits bytecode and then uses a C preprocessor to expand the macros
into 6502 assembler.
gcc6809 - need to check this out
SmallerC - https://github.com/alexfru/SmallerC
vbcc - Targets 68K, patch for z-machine
http://sun.hasenbraten.de/vbcc/
OTHER COMPILERS
PLASMA - Language + VM, sort of like SWEET16 meets
Java. Simple, concise code base. Pretty fast too, in the same ballpark as
CC65. Focused on the Apple I/II/III family.
Atalan - Rule-based language/assembler with support for multiple 8-bit
platforms. Looks promising, but I had problems with it crashing, and it
lacks documentation/test cases. Atari-focused.
Macross/Slinky - A macro cross-assembler developed by Chip Morningstar at
Lucasfilm Ltd. for use in the game Habitat. Billed as "An assembler for
people who hate assembly language."
NESHLA - A high-level assembler in the same spirit as Macross, but with
NES-specific features.
WLA-DX - Some other weird cross-assembler with lots of CPU targets.
z88dk - A Small C-derived cross-compiler for the Z80, supporting 20 target
machines.
https://github.com/EtchedPixels/FUZIX/wiki
Java Grinder - Compile Java bytecode to microcontroller assembly. Currently
supporting MSP430, dsPIC, 6502/6510, 68000, MIPS, TMS9900, and Z80 with
platforms that include Commodore 64, Sega Genesis, Atari 2600, Apple IIgs,
ChipKit (PIC32), TI99, TI84 and more.
ZX Basic - This cross-compiler is focused on ZX-Spectrum, but with some
patchworking it can be used to compile to whatever machine running on z80
architecture. (Python)
IntyBASIC: a BASIC compiler for Intellivision. IntyBASIC is a cross
compiler that takes BASIC source code and translates it to CP1610 assembler
code. http://nanochess.org/intybasic.html
SWEET16 - Woz's tiny bytecode interpreter on the Apple ][ integer BASIC ROM.
Still emcumbered by Apple's copyright for the foreseeable future.
http://6502.org/source/interpreters/sweet16.htm
Quetzalcoatl - http://www.kdef.com/geek/vic/quetz.html
UC65 - https://code.google.com/archive/p/uc65/
Cowgol - https://github.com/davidgiven/cowgol
AcheronVM - https://github.com/AcheronVM/acheronvm
-90
View File
@@ -1,90 +0,0 @@
# Kernel
# A Kernel draws a set of scanlines to the screen.
component Kernel
lines: 0..255 "Height of region in scanlines"
bgcolor: 0..255 "Background color"
end
system SimpleKernel
tempbytes 8
on preframe do once [Kernel] ---
lda #192 ; TODO: numlines
sec
sbc ypos_ypos_b0+ent
sta {{$5}}+ofs
ldy hasbitmap_bitmap_b0+ent
lda bitmap_bitmapdata_b0,y
sec
sbc {{$5}}+ofs
sta {{$0}}+ofs
lda bitmap_bitmapdata_b8,y
sbc #0
sta {{$1}}+ofs
ldy hascolormap_colormap_b0+ent
lda colormap_colormapdata_b0,y
sec
sbc {{$5}}+ofs
sta {{$2}}+ofs
lda colormap_colormapdata_b8,y
sbc #0
sta {{$3}}+ofs
lda sprite_height_b0+ent
sta {{$4}}+ofs
lda ypos_ypos_b0+ent
sta {{$5}}+ofs
---
on preframe do once [Sprite,HasBitmap,HasColormap,HasYpos] --
{{@KernelSetup}} 0,0
{{@KernelSetup}} 1,6
--
on kernel do once [Kernel]:
lda %{<bgcolor}
sta COLUBK
ldy %{<lines}
@LVScan:
lda %{$4} ; height
dcp %{$5}
bcs @DoDraw1
lda #0
.byte $2C
@DoDraw1:
lda (%{$0}),y
sta WSYNC
sta GRP0
lda (%{$2}),y
sta COLUP0
lda %{$10} ; height
dcp %{$11}
bcs @DoDraw2
lda #0
.byte $2C
@DoDraw2:
lda (%{$6}),y
sta GRP1
lda (%{$8}),y
sta COLUP1
dey ; decrement
bne @LVScan ; repeat until 192 lines
--
end
scope Root
entity kernel [SimpleKernel]
const lines = 100
end
entity player1 [Sprite,HasBitmap,HasColormap,HasYpos]
const plyrindex = 0
init height = 8
init xpos = 100
init ypos = 100
end
end scope
-579
View File
@@ -1,579 +0,0 @@
TODO:
- NaN in cycle count for macros
- debugging of scan line overflow
- confuse code/data in listing
- show memory locations hovering over lines
- don't check against ROM signatures
- DASM
- macro forward refs
- labels start with . omit timing info (isCode)
- spaces in filename don't parse code listing (DASM, maybe more)
- asm: support macro expansion
- multiple breakpoints, expression breakpoints
- watchpoints
- step over (line, instruction)
- slowdown beam for all platforms?
- intro/help text for each platform
- show self-modifying code insns left of editor
- update Javatari version? (and others?)
- sound mute?
- $error updates source editor
- online tools for music etc
- text log debugging script
- vscode/atom extension?
- click to break on raster position
- debug bankswitching for funky formats
- 'undefined' for bitmap replacer
- requestInterrupt needs to be disabled after breakpoint?
- C/asm source formatter
- allow download of JSASM output
- update bootstrap to 4.0
- batariBasic: proper line numbers, debugging
- builds:
- compiler flags for final ROM build
- workermain: split build functions, better msg types
- what if >1 file with same name? (local/nonlocal/directory)
- what if .c and .s names collide?
- maybe put stuff in examples/ dir?
- error msg when #link doesn't work
- warning when ROM too big
- detect "libcv.h" and include library automagically?
- sdcc:
- can't link asm files before c files (e.g. acheader.s must be last)
- figure out area names ordering
- debug inline asm
- live coding URL
- resize memory browser, other windows when vertical div resize
- markdown, verilog: can't share
- https://www.crowdsupply.com/tinyfpga/tinyfpga-bx
- stego shareable images (http://pico-8.wikia.com/wiki/P8PNGFileFormat)
- https://makecode.com/language?
- open ROM from URL?
- game starts even if switched away before first load
- put globals into view/controller objects
- autostart audio
- chrome: https://github.com/processing/p5.js-sound/issues/249
- firefox: https://support.mozilla.org/en-US/kb/block-autoplay
- touch support
- better undo/diff for mistakes?
- ide bug/feature visualizer for sponsors
- global undo/redo at checkpoints (when rom changes)
- landscape mode for arcade ports
- symmetric load/save state types (like in Machine)
- pixel editor
- persist palette/tilemap selections
- more tools for editing
- map editor
- arbitrary nametable editing
- metasprites
- throw errors when bad/no refs
- per-View keyboard shortcuts
- parse labels
- parse .incbin directives?
- can't replace in hex directives
- should maybe use same single-canvas editor for map + char editor
- undo doesn't refresh editor
- editing sometimes messes up cursor movement (CURBS)
- crt0.s compiled each time?
- debug highlight doesn't go away when debugging -> running
- show breakpoint of PC or highest address on stack
- can we highlight line instead of select?
- profiler
- profiler restarts emulator when paused
- running profiler while replaying? grand unified replay?
- click on profiler to step to position
- breakpoints stop profiler from running
- single-screen profiler
- hide labels that aren't available, like BIOS addrs
- show interrupts, other events
- sometimes interleaves two different PCs? like two profilers running simultaneously?
- ah, symbols persist across builds
- Verilog
- larger scope range, better scrolling
- make scope data wrap around range
- remove FPS and play controls when Verilog scope paused
- when paused scope doesn't work
- Safari: scope doesn't show while CRT in use (sometimes Firefox too)
- verilog debugging/reloading makes it slow
- why loadState() on verilog kill perf?
- quantify verilog "graph iterations"
- CPU debugging
- use $readmem for inline asm programs?
- can't add control instructions b/c of split
- bad error msg if >2 moduels and top module doesn't match filename
- separate Scope View
- Audio doesn't work if clock != default
- Verilog WASM not yet supported:
- wide numbers > 64 bits (and in scope view)
- multiple modules instances
- structs
- other $funcs
- multidim arrays
- optimize trace log w/ wasm buffer
- yosys compatibility
- randomize on reset? (https://www.xilinx.com/support/documentation/white_papers/wp272.pdf)
- XML should tell us which values are supposed to reset
- single-stepping vector games makes screen fade
- break on stack overflow, illegal op, bad access, BRK, etc
- show in scope instead?
- nes
- replay doesn't work for nes (force background tile redraw, sprites
messed up)
- nes debug view toolbar
- support NES_HEADER_16K?
- NES crt should mark raster pos when debugging
- OAMDMA in profiler? (haltCycles)
- typed arrays don't serialize?
- JSNES
- doesn't support hiding >8 sprites
- doesn't do sprite zero test right
- doesn't do clip right
- doesn't do b/w tint
- vcs
- sometimes still plays when paused on FF
- vcs: INPTx needs to be added to control state
- vcs: break on # of lines changed (maybe using getRasterPosition?)
- chrome looks blurry on vcs
- VCS asm game library
- VCS skips step on lsr/lsr after run to line
- better VCS single stepping, maybe also listings
- upgrade to 4.1 for Supercharger format
- upload multiple files/zip file to subdirectory
- allow "include graphics.asm" instead of "include project/graphics.asm"
- convert more stuff to Promises
- don't have to include firebase always?
- squelch error msgs?
- test offline? (if window.firebase)
- Github
- platform_id/repo.platform mismatch (can't leave repository)
- handle overwrite logic
- what to do about included files?
- can published files retain path?
- what if import interrupted and partial files?
- CORS for some blobs?
- don't import useless files
- support projects with subdirectories, file list?
- switching platform of a repo?
- make sure to flatten subdirs
- re-publish repo (bug: doesn't put all files in local repo after publishing)
- allow text/binary change
- importing from subtree commits to root anyway
- publishing Markdown file loads default file?
- better text/binary detection (e.g. 0xa9 copyright)
- going into repo chooses wrong file if republished with different main
- renaming main file doesn't change repo def.
- can't edit or push files until successful compile
- changing platform of repo makes one platform redirect to another platform
- should be able to change vcs -> vcs.mame for exmaple
- keyboard shortcuts
- ctrl+alt+l on ubuntu locks screen
- alt-D doesn't work anymore
- facade/kbd shortcuts for emulators, focus
- maybe eat key events? ("'" messes things up)
- cookie
- list of stuff for policy
- popup
- convert binary to hex stmts
- "suggestions" (vblank overrun, variable # scanlines, etc)
- SMS
- can't step back twice?
- compiler bug in chase
- "shared" in URL doesn't work, leave in URL? (also importURL)
- 6502
- KIL stops debugger
- TypeError: null is not an object (evaluating 'n.destination')
https://8bitworkshop.com/v3.4.1/javatari.js/release/javatari/javatari.js
(32:443651) Safari 12.1.2
- Safari: doesn't send good exception reasons ("undefined")
- probably XMLHttpRequest.timeout (https://github.com/getsentry/sentry-javascript/issues/2210)
- C64
- disk image support
- https://github.com/cc65/cc65/issues/946
- player 2 joystick and keyboard conflict
- problems with cgetc()
- upgrade to ES2020?
- https://github.com/microsoft/TypeScript/issues/16577
- https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules
- need Edge 79+
- no import in workers
- copy to gen/ directory (allowJs is weird)
- ca65 line numbers are not aligned with source code
- neither are DASM when you change comments
- segments not read properly
- Debug Browser
- more stuff like 7800 display lists
- don't include start/end line on platforms w/o raster timing
- BIOS symbols
- show current tool for file
- download non-text incbin source file
- show hidden header files that only exist in Emscripten FS
- WASI modules: https://docs.wasmer.io/integrations/js/wasi/browser
- FS Access: https://web.dev/file-system-access/
- https://wicg.github.io/file-system-access/#api-showdirectorypicker
Probing
- probe log doesn't start @ reset
- scroll to current probe log location
Debug, play then halt cpu doesn't highlight final line
- illegal instruction should show debug info + jump to line
- halt cpu should show video too
vcs: input not preserved when replaying frames?
compile errors disappear when focus returns
Stellerator
- window eats keystrokes (e.g. "i")
- more features, debugging
WEB WORKER FORMAT
{code,platform,tool,dependencies}
{preload}
new:
{updates,buildsteps}
[{filepath,platform,tool}]
implicit rules
- push/shift build steps
- preprocessor, compile, assemble, link
- access to intermediate files
only build files that have changed
build options
BUILD PROJECT
local/foo.project/*.*
build all files in project
send update for dependencies at startup (single file version)
should be able to change file w/o reloading (Not platform tho)
pulldown selects file in project, or goes back to parent
gotta preserve editor windows for each one - window list
make source/disasm window visible when debugging
gutter info/errors in all files
UI REFACTOR
editor
debugger
disasm view
memory
profile
errors
separate into files
window methods:
- set
- mode
- get
- changes
- cursorActivity (inspect)
- gutters (setCompileOutput)
- current line / gutter marker (setCurrentLine / clearCurrentLine)
- source map
- global symbols
- pixel editor / find comment (openBitmapEditorAtCursor)
- update debug window (200 ms)
- inspect variable
file store mirrors that on worker
worker result:
{
output:<rom>
sources:{
<path>:{lines, asmlines, listing}
}
}
add file to errors
http://www.playvectrex.com/designit/chrissalo/vectordisplay.htm
http://oldies.malban.de/secondvectrex/index.htm
PIXEL EDITOR
everything is an editor
back/forward propogation
encoded <-> raw + spec + palette(s) <-> full tilemap <-> selected tile/sprite/metasprite
need references
panes: source code, metadata tree, graphics panes
decoded info:
- CHR tiles
- palettes
- maps/rooms
- metasprites
- metatiles
- sprite data (williams etc)
store in intermediate single file or embed in C/ASM?
we'd then have to generate C/ASM probably automatically
intermediate file could still be edited
switch between hex/graphic editor? (palettes should edit via hex)
final display types: number, color, sprite, etc
CHR editor has palette selector, obj selector
individual objects in single file or multiple files?
RLE compression
Types
- byte
- RGBA color
- structured object
- metasprite
- metatile
- level actor
- bitmap
- array of stuff, array can have a shape (width,height) like numpy
Mappers can refer to multiple objs
Slice (window) of array for selections
AssetEditorView
- scan files for assets
- build hierarchical view of assets
- expand/collapse
- create editor upon expansion
- double-click to source file
Assets come from:
- chr files
- bitmap data in files
- structured data (palette, sprites, metasprites, levels, etc)
- think about new comment format, platform-specific types
Programmatic Asset Language
- load PNG, draw image, or generate bitmap font
- split into images
- paletteize
- fit into tileset/spriteset
- NES: 8x8 or 8x16 tiles
- 7800: 256xN slice
- VCS: 24xN playfield, 8xN sprite, 32xN big sprite
- generate data file (asm/c/bin)
- generate char mapping
- tile generator (makechr-like)
- metasprites too
- RLE compression
- check constraints
https://github.com/endojs/endo/blob/master/packages/ses/README.md
- CPU-independent IR
- structured assembler IR
- ECS/macros module
- HLL
- static analysis module
- linker/placement module
- inlining, data reuse
- nested tag for segments
- binary generation module
- remote fetch module
- asset module (bitmaps, sprites, tiles, palettes, compression...)
- asset editor UI
- debugging API (custom state, per-cycle watchpoints)
---
Github Support
Import
Export
Login
Pull
Push
Git metadata kept in local storage
Converting from NESASM to DASM
- asl a -> asl
- subroutine keyword on labels
- [zp],y to (zp),y
- LOW(x) and HIGH(x) to <() and >()
- .db to .byte, .dw to .word
- use NES_HEADER macros
- no .bank
Cross platform NES/SMS/GG library
- use CHR RAM
- create flipped tiles/sprites
- create alternate palette tiles
- metatiles
- cross platform music/sound
- scrolling
- row/column mask
- no nametable mirroring in SMS
- 256x240 vs 256x192
Emulator Lib
- move getPresets() (into presets/ dir?)
- CPU interface
- execCycle(), execInsn()
- fix/unfix PC
- interrupt
- generic raster scanline platform
- PlatformRunner
- handles rewind, intra-frame breakpoint, debugging
- profiling log, exec/read/write/intr (for each bus?)
- expose video, audio, controller interfaces
- new debugging info
- memory map interface
- "About" metadata
- auto load/save state?
- handle legacy
- VCS
- NES
- MAME
- MAME
- debugging?
- kbd instructions don't work
- https://emscripten.org/docs/porting/asyncify.html
Markdown (showdown) interface
- add code patches to files
- cursor shows current state/errors (must map markdown->src lines)
- show preview emulator at each step (play button? run when viewed?)
DOCUMENTATION
memory map
BIOS routines
cc65 standard libs
cc65 headers
example headers (presets/*/*.h)
libcv headers (src/worker/lib)
ASM includes
NETPLAY
runs alongside of emulator, doesn't modify controls (just state)
when controls change: player sends control inputs + frame#
add latency so clients more often are in sync
if miss latency window, client syncs state (moves forward a few frames?)
state checksum?
socket.io
X86
https://github.com/freebasic/fbc
GW-BASIC
http://www.grifo.com/SOFT/Pacific/uk_pacific.html
http://www.cpm.z80.de/small_c.html
https://github.com/open-watcom
Free Pascal
https://bellard.org/tcc/
https://wiki.osdev.org/Smaller_C
https://yasm.tortall.net/
https://wiki.osdev.org/Tool_Comparison
COMPONENT EMULATION
Emulate discrete chips
- verilog
- js
- wasm? (overhead?)
Block diagram level
Buses
Insn/cycle timing
Pos/neg clock edge
Timers
Polling vs. callbacks (https://github.com/floooh/chips)
- callbacks might only affect devices internally, until next cycle
CRT simulator
Move leaf nodes to workers (sound chips)
CPU opcode-step switch: https://floooh.github.io/2019/12/13/cycle-stepped-6502.html
- macros are cool!
Inspect bus edges and internal state
type Bits<N> = number;
interface Clocked { }
interface BusListener<N> {
recv(value : Bits<N>) : void;
}
class Bus<N> {
listeners : BusListener<N>;
send(value : Bits<N>) : void {
}
}
function bus(bits : number) {
return function(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
//descriptor.configurable = value;
};
}
class M6502_T {
IR : Bits<16>; /* internal instruction register */
addr : Bus<16>;
data : Bus<8>;
@bus(16) address : Bus<16>;
}
Optimizations
- ordered/delayed assignments
- skip unused exprs
- opcode -> fn table
- bus protocols -> functions
- state -> lookup tables or switch statements
- vec[] -> bitmask
- multiclock execution?
STATE REWIND
in Platform:
advance?(novideo? : boolean) : number;
advanceFrameClock?(trap:DebugCondition, step:number) : number;
in devices:
advanceFrame(trap:TrapCondition) : number;
advanceClock() : void;
advanceInsn() : number;
Should call trap() every cycle or insn of frame, or exit when returns true?
BETTER DEBUGGING
Know if we are actively debugging or trap occurred
isRunning() = stopped, running, waiting, debugging...
Showing running PC may be distracting, maybe lines visited?
Don't grab cursor focus when trap occurs (how do we know?)
Use tick() and refresh(), not callbacks
Show current datum when using READ
Use https://codemirror.net/doc/manual.html#markText
Reset doesn't break @ start unless debugging tools expanded
Single-step can un-sync frame/scanline timing
PORTING CC65 TO IDE
flat directory for .c files
#include "file.h" vs <file.h>
//#link "foo.c" for each C file
preprocessor differences (#define FOO 1 so #if works)
extern void do_func(void)
#pragma warnings
CFG files
-49
View File
@@ -1,49 +0,0 @@
#!/usr/bin/python
#
# C header file -> Markdown-ish converter
import sys,re
re_define = re.compile("^#define\s+(\w+)([(][^)]+[)])?")
re_func = re.compile("^(\w+\s+[A-Za-z_ ]+[(][^)]+[)]);")
re_comment1 = re.compile("//(.+)\\\\?")
comment = ''
def dump(s):
global comment
print('`'+ s.strip() +'`')
print(': ' + comment.strip())
comment = ''
files = sys.argv[1:]
for fn in files:
with open(fn,'r') as f:
print("## " + fn)
l = f.readline()
while l:
l = l.strip()
m1 = re_define.match(l)
m2 = re_func.match(l)
m3 = re_comment1.match(l)
if m3:
comment += m3.group(1)
if m1:
s = m1.group(1)
if m1.group(2):
s += m1.group(2)
dump(s)
elif m2:
s = m2.group(1)
s = s.replace('void ','').replace('(void)','()')
s = s.replace('__fastcall__',' ')
s = s.replace(' ',' ')
s = s.replace(' ',' ')
dump(s)
elif not m3:
if l != '':
print('<!--' + l + '-->')
else:
print('')
l = f.readline()
-265
View File
@@ -1,265 +0,0 @@
class Platform
--------------
Mandatory functions:
~~~
start() : void;
reset() : void;
isRunning() : boolean;
pause() : void;
resume() : void;
loadROM(title:string, rom:any);
~~~
These are for the compiler/editor:
~~~
getToolForFilename(s:string) : string;
getDefaultExtensions() : string[];
getPresets() : Preset[];
~~~
Most platforms have these:
~~~
loadState?(state : EmuState) : void;
saveState?() : EmuState;
~~~
... etc
6502
----
`advance()` advances one frame.
The basic idea: iterate through all the scanlines, run a bunch of CPU cycles per scanline.
If we hit a breakpoint, exit the loop.
~~~
var debugCond = this.getDebugCallback();
for (var sl=0; sl<262; sl++) {
for (var i=0; i<cpuCyclesPerLine; i++) {
if (debugCond && debugCond()) {
debugCond = null;
sl = 999;
break;
}
clock++;
cpu.clockPulse();
}
}
~~~
Hitting a breakpoint does a `saveState()` but debug info is better when the platform is stopped at the breakpoint instead of being allowed to continue.
Some platforms like `vector` aren't scanline-based, they just have a target number of scanlines per frame (per 1/60 sec)
The 6502 CPU core is usually a byte behind the current instruction at breakpoints.
So when saving state we +1 the PC by calling `fixPC`.
When loading state we have to -1 the PC, load state, then +1 the PC.
~~~
this.unfixPC(state.c);
cpu.loadState(state.c);
this.fixPC(state.c);
~~~
Z80
---
There's a `runCPU()` wrapper:
~~~
advance(novideo : boolean) {
for (var sl=0; sl<scanlinesPerFrame; sl++) {
drawScanline(pixels, sl);
this.runCPU(cpu, cpuCyclesPerLine);
}
// NMI each frame
if (interruptEnabled) { cpu.nonMaskableInterrupt(); }
}
~~~
Atari 2600
-----------
8bitworkshop was originally VCS-only, Javatari.js was the first emulator it supported.
It's a wonderful emulator, but it didn't have hooks for debugging.
I had to hack it up quite a bit, and wasn't sure what I was doing.
A lot of the debugging functions just pass-through to my hacked-up functions:
~~~
step() { Javatari.room.console.debugSingleStepCPUClock(); }
stepBack() { Javatari.room.console.debugStepBackInstruction(); }
runEval(evalfunc) { Javatari.room.console.debugEval(evalfunc); }
~~~
Even so, I decided to monkey-patch the `clockPulse()` function so that I could record frames:
~~~
Javatari.room.console.oldClockPulse = Javatari.room.console.clockPulse;
Javatari.room.console.clockPulse = function() {
self.updateRecorder();
this.oldClockPulse();
}
~~~
Eventually I'd like to make it more like the other platforms.
8bitworkshop uses its CPU core for other 6502 platforms.
BasicZ80ScanlinePlatform
------------------------
Can be used to easily build a Z80-based raster platform.
Just have to fill out the following:
~~~
cpuFrequency : number;
canvasWidth : number;
numTotalScanlines : number;
numVisibleScanlines : number;
defaultROMSize : number;
abstract newRAM() : Uint8Array;
abstract newMembus() : MemoryBus;
abstract newIOBus() : MemoryBus;
abstract getVideoOptions() : {};
abstract getKeyboardMap();
abstract startScanline(sl : number) : void;
abstract drawScanline(sl : number) : void;
getRasterScanline() : number { return this.currentScanline; }
getKeyboardFunction() { return null; }
~~~
NES
---
NES uses the JSNES emulator, which has a callback function after each frame.
~~~
this.nes = new jsnes.NES({
onFrame: (frameBuffer : number[]) => {
},
onAudioSample: (left:number, right:number) => {
},
onStatusUpdate: function(s) {
},
});
~~~
We monkey-patch the code to add a debugging hook:
~~~
// insert debug hook
this.nes.cpu._emulate = this.nes.cpu.emulate;
this.nes.cpu.emulate = () => {
var cycles = this.nes.cpu._emulate();
this.evalDebugCondition();
return cycles;
}
~~~
NES was the first platform with an "illegal opcode" hard stop, so we added a special `EmuHalt` exception which causes a breakpoint:
~~~
this.nes.stop = () => {
console.log(this.nes.cpu.toJSON());
throw new EmuHalt("CPU STOPPED @ PC $" + hex(this.nes.cpu.REG_PC));
};
~~~
MAME
----
The `BaseMAMEPlatform` class implements a MAME platform.
You just have to pass it various parameters when starting, and tell it how to load the ROM file:
~~~
class ColecoVisionMAMEPlatform extends BaseMAMEPlatform implements Platform {
start() {
this.startModule(this.mainElement, {
jsfile: 'mamecoleco.js',
cfgfile: 'coleco.cfg',
biosfile: 'coleco/313 10031-4005 73108a.u2',
driver: 'coleco',
width: 280 * 2,
height: 216 * 2,
romfn: '/emulator/cart.rom',
romsize: 0x8000,
preInit: function(_self) {
},
});
}
loadROM(title, data) {
this.loadROMFile(data);
this.loadRegion(":coleco_cart:rom", data);
}
getPresets() { return ColecoVision_PRESETS; }
getToolForFilename = getToolForFilename_z80;
getDefaultExtensions() { return [".c", ".ns", ".s", ".scc", ".sgb", ".z", ".wiz"]; }
}
~~~
A lot of things are done via Lua scripting -- for example, loading a ROM requires we loop over the memory region and issue `rgn:write_u32` calls.
It kinda-sorta works, except debugging isn't reliable because MAME [doesn't return from the event loop](https://github.com/mamedev/mame/issues/3649) at breakpoints.
MAME platforms don't have state load/save either.
Verilog
--------
The Verilog platform is the odd one out, since it has no fixed CPU as such.
The `loadROM` function instead loads a JavaScript function.
Some platforms do have a ROM if using assembly, so we load that into a Verilog array.
It's quite the hack, and it could be better.
Verilog has its own debugger, logging signals in a fixed-size buffer.
Profiling
----------
`EmuProfilerImpl` runs the profiler.
When started, it calls `setBreakpoint` to add a profiler-specific breakpoint that never hits, just records the CPU state at each clock.
It uses `getRasterScanline` to associate IPs with scanlines.
Platforms can also log their own reads, writes, interrupts, etc.
Future Ideas
------------
There should be a standard CPU interface, buses, memory map.
More like MAME configuration.
Platforms might have different ideas of "clock" (CPU clock, pixel clock, 1 clock per instruction, etc)
The goal is to rewind and advance to any clock cycle within a frame, and get complete introspection of events, without hurting performance.
Unify raster platforms, they should all allow the same debugging and CPU interfaces.
Separate UI/sim parts of platform?
A lot of platforms write into a uint32 buffer.
We might want to buffer audio the same way.
Also some way to log events, and handle input.
Figure out how to make platform-specific type for load/save state.
(generics?)
Separate emulators from 8bitworkshop IDE.
Can we use WASM emulators without JS interop penalty?
Maybe using [AssemblyScript](https://docs.assemblyscript.org/)?
Startup would be faster, probably runtime too.
Drawback is that dynamic stuff (custom breakpoint functions, profiling) might be slow, slower dev too maybe.
Need proof-of-concept.
-6
View File
@@ -110,12 +110,6 @@ body {
<li class="dropdown dropdown-submenu">
<a tabindex="-1" href="javascript:void(0)">Help</a>
<ul class="dropdown-menu" id="help_menu">
<li><a class="dropdown-item" target="_8bws_help" href="https://8bitworkshop.com/docs/docs/ide.html">IDE Help</a></li>
<li><a class="dropdown-item" href="javascript:void(0)" id="item_keyboard_shortcuts">Keyboard Shortcuts</a></li>
<li><a class="dropdown-item" href="javascript:void(0)" id="item_asset_editor_help">Asset Editor Reference</a></li>
<!--
<li><a class="dropdown-item" href="javascript:void(0)" id="item_tool_versions">Toolchain Versions</a></li>
-->
</ul>
</li>
<li class="dropdown dropdown-submenu">
+103
View File
@@ -53,6 +53,7 @@
"fast-png": "^5.0.4",
"jsdom": "^25.0.1",
"lzg": "^1.0.x",
"markdown-it": "^15.0.2",
"mocha": "^10.7.3",
"mocha-simple-html-reporter": "^2.0.0",
"mocha-suppress-logs": "^0.6.0",
@@ -5647,6 +5648,26 @@
"immediate": "~3.0.5"
}
},
"node_modules/linkify-it": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-6.1.0.tgz",
"integrity": "sha512-wJ/TwpSDTLepCrQoYWYIExIKg5Zchex2Nn5yk2mFnB+6PtdkHtyLx742md9csRjjOnGkKIS/RrbY7l8D6gT9Vw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"uc.micro": "^3.0.0"
}
},
"node_modules/load-json-file": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz",
@@ -5899,6 +5920,64 @@
"semver": "bin/semver"
}
},
"node_modules/markdown-it": {
"version": "15.0.2",
"resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-15.0.2.tgz",
"integrity": "sha512-q4IGxMv56jCqT4OCRCADBoDP3LO4MhmTXjFbphHPXs4g3j9Xg5RDnxqN8IF/3vIWEU+VCnUq+7JUg/cfy2E6Qw==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/markdown-it"
}
],
"license": "MIT",
"dependencies": {
"argparse": "^3.0.0",
"entities": "^8.0.0",
"linkify-it": "^6.0.0",
"mdurl": "^2.1.0",
"punycode.js": "^2.3.1",
"uc.micro": "^3.0.0"
},
"bin": {
"markdown-it": "bin/markdown-it.mjs"
}
},
"node_modules/markdown-it/node_modules/argparse": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-3.0.2.tgz",
"integrity": "sha512-mFdDM6WqWKraGLsVb+C9CahPnzTXOefAOLq3jYcca2YZ8bEWpr++Tzj+zSaKW9+X9L5uSxcm1AZ3Y6aZJ09OhQ==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/puzrin"
},
{
"type": "github",
"url": "https://github.com/sponsors/nodeca"
}
],
"license": "PSF-2.0"
},
"node_modules/markdown-it/node_modules/entities": {
"version": "8.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-8.1.0.tgz",
"integrity": "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=20.19.0"
},
"funding": {
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5909,6 +5988,13 @@
"node": ">= 0.4"
}
},
"node_modules/mdurl": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz",
"integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==",
"dev": true,
"license": "MIT"
},
"node_modules/media-typer": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
@@ -7692,6 +7778,16 @@
"node": ">=6"
}
},
"node_modules/punycode.js": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz",
"integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qs": {
"version": "6.15.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.15.3.tgz",
@@ -9089,6 +9185,13 @@
"typescript": "^2.1.6 || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev"
}
},
"node_modules/uc.micro": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-3.0.0.tgz",
"integrity": "sha512-U3PppEkleoTnIfi8BozMx3yju3qc/L6SwqWo2Sw+54PX+PX0q9I+r1Um5HCmqD7n9VDX5/v3vQH/AjA6deDdtw==",
"dev": true,
"license": "MIT"
},
"node_modules/undici-types": {
"version": "6.21.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
+5 -4
View File
@@ -54,6 +54,7 @@
"fast-png": "^5.0.4",
"jsdom": "^25.0.1",
"lzg": "^1.0.x",
"markdown-it": "^15.0.2",
"mocha": "^10.7.3",
"mocha-simple-html-reporter": "^2.0.0",
"mocha-suppress-logs": "^0.6.0",
@@ -81,11 +82,11 @@
"test": "npm run tsbuild && npm run test-node",
"tsbuild": "tsc --build tsconfig.json test/tsconfig.json",
"tslint": "tsc --build -noEmit tsconfig.json test/tsconfig.json",
"esbuild": "npm run esbuild-worker && npm run esbuild-ui && npm run esbuild-server",
"esbuild": "node build/build.mjs all",
"esbuild-clean": "rm -f ./gen/*.*",
"esbuild-server": "esbuild src/worker/server/server.ts --platform=node --bundle --sourcemap --target=es2020 --outfile=./gen/server/server.js",
"esbuild-worker": "esbuild src/worker/workermain.ts --bundle --sourcemap --target=es2017 --outfile=./gen/worker/bundle.js",
"esbuild-ui": "esbuild src/ide/ui.ts src/ide/embedui.ts --splitting --format=esm --bundle --minify --sourcemap --target=es2017 --outdir=./gen/ --external:path --external:fs",
"esbuild-server": "node build/build.mjs server",
"esbuild-worker": "node build/build.mjs worker",
"esbuild-ui": "node build/build.mjs ui",
"server": "npm run esbuild-server && rm -fr ./server-root/sessions && node gen/server/server.js",
"test-one": "NODE_PATH=$(pwd) mocha --recursive --timeout 60000",
"test-node": "NODE_PATH=$(pwd) mocha --parallel --require mocha-suppress-logs --recursive --timeout 60000 test/cli gen/test",
+404
View File
@@ -0,0 +1,404 @@
# Asset Header
An **asset header** is a tiny note to the [Asset Editor](asseteditor.md)
that tells it what a block of data means. It is just a JSON object inside
a comment, sitting right above the bytes:
```c
/*{w:8,h:8,bpp:1,count:2,brev:1}*/
const byte tiles[] = { ... };
```
From that short comment the editor knows the data is two 8×8, 1-bit
images, most significant bit on the left. You write the header by hand
once, and from then on the Asset Editor can draw and edit the data for
you.
## Header syntax
A header is a comment containing a single JSON object, immediately
followed by a data block. The block ends at the next closing delimiter,
which depends on the language:
| Language | Header | Data block ends at |
| --- | --- | --- |
| C / C++ | `/*{w:8,h:8}*/` | next `;` |
| Assembly | `;;{w:8,h:8};;` | next `;;` |
| Verilog | `/*{w:8,h:8}*/` | next `end` |
**Each number needs a radix prefix so the editor can read it.** It
recognizes `0xNN`, `$NN`, `#$NN`, `%0101`, `0b0101`, `8'hNN`, `8'b0101`,
and assembler `hex 1f20…` statements. It also remembers the notation you
used and writes edits back the same way, so a file of `$`-prefixed bytes
stays `$`-prefixed. The one thing it will *not* read is **plain
decimal** — a block of `{24,60,126,…}` parses as zero values and reports
*"Expected 8 value(s), found 0"*.
```c
/*{w:8,h:8,bpp:1,count:2,brev:1}*/
const byte tiles[] = {
0xff,0x81,0x81,0x81,0x81,0x81,0x81,0xff,
0x18,0x3c,0x7e,0xff,0xff,0x7e,0x3c,0x18,
};
```
```asm
;;{w:8,h:8,count:1,brev:1,flip:1};;
PlayerGfx:
.byte $18,$3c,$7e,$ff,$ff,$7e,$3c,$18
;;
```
### Writing JSON loosely
You don't have to quote the keys. The scanner rewrites `([A-Za-z]+):` to
`"$1":` before parsing, so `w:8` and `"w":8` mean the same thing. String
*values* still need their quotes, though: `pal:"nes"`, `comp:"rletag"`.
### Things to watch out for
- **The first `;` wins.** In C the block ends at the very next semicolon,
so the array declaration must be the only statement between the header
and that `;`. Anything else and the editor reads the wrong data.
- Keys are case-sensitive and must be alphabetic.
- A malformed header is reported inline in the Asset Editor tab, rather
than being silently ignored.
### Reading data from another file with `#embed`
If the data block contains an `#embed` directive, the bytes are read
from that file instead of from the source text:
```c
/*{w:24,h:21,bpp:1,brev:1,wpimg:64,count:1}*/
const char sprite[] = {
#embed "sprite.bin"
};
```
The path is resolved first exactly as written, then relative to the
directory of the file containing the header (`sub/main.c` + `data.bin` →
`sub/data.bin`). The file's byte length is validated against the format
the same way inline data is.
## Field reference
### Image fields
These describe a bitmap or character set. Only `w` and `h` are required —
everything else falls back to a sensible default.
| Field | Default | Description |
| --- | --- | --- |
| `w` | *required* | Width in pixels |
| `h` | *required* | Height in pixels |
| `count` | `1` | Number of images |
| `bpp` | `1` | Bits per pixel |
| `np` | `1` | Number of bitplanes (colors = 2<sup>bpp&times;np</sup>) |
| `bpw` | `8` | Bits per word (8, 16, 32) |
| `sl` | `ceil(w*bpp/bpw)` | Words per scanline (stride) |
| `wpimg` | `sl*h` | Words per image — use when hardware pads images |
| `pofs` | `sl*h*count` | Distance between bitplanes, in words |
| `skip` | `0` | Words to skip at the start of the whole block |
| `brev` | `false` | MSB is the leftmost pixel |
| `flip` | `false` | Data is stored bottom row first |
| `il` | `false` | Interleave images row by row |
| `remap` | — | Bit permutation applied to the word offset |
| `reindex` | — | Per-column word/bit table |
| `aspect` | `1` | Pixel aspect ratio for display only |
| `art` | `false` | Apple II HGR artifact-color mode |
| `comp` | — | `"rletag"` = RLE-compressed block |
| `map` | — | `"nesnt"` = NES nametable, not a bitmap |
| `pacstrip` | — | Pac-Man/Namco vertical-strip layout |
| `xform` | — | CSS transform. Parsed and stored but currently inert |
### Palette fields
Give a block a `pal` and no `w`/`h`, and it becomes a palette editor
instead of an image editor.
| Field | Default | Description |
| --- | --- | --- |
| `pal` | — | Palette decoding, see below |
| `n` | — | Advisory entry count — the real count comes from the data |
| `layout` | — | Grouped editor layout: `nes`, `astrocade`, `pacman` |
`pal` is either:
- **A number** like `332` or `444` — the three digits are channel bit
widths, applied **from the low bits upward**. A positive value puts red
lowest (`pal:332` = RGB332, red in bits 0–2); a **negative** value puts
blue lowest (`pal:-332` = blue in bits 0–2, red in the top 2 bits).
The picker's color space is `1 << (sum of digits)` entries, which is why
`pal:444` is a 4096-color chooser regardless of how many entries the
block holds.
- **A name**: `"nes"`, `"vcs"`, `"c64"`, `"gb"`, `"ap2lores"`,
`"astrocade"`, or `"pacman"` (Namco color-PROM bytes, decoded with
MAME's resistor weights). These are the only recognized names — an
unknown one reports `No palette named X`.
Bitmaps pick up their colors from palette blocks **anywhere in the open
project**, matched by entry count — a 4-color bitmap offers every 4-entry
palette (and every 4-entry slice named by a `layout`) in a dropdown. Game
Boy projects fall back to the DMG green scale when no palette block
matches.
```c
/*{pal:"nes",layout:"nes"}*/
const char PALETTE[8] = { 0x0F, 0x11,0x24,0x3C, 0x00, 0x01,0x15,0x25 };
```
`bpw` applies to palette blocks too — PC Engine palettes are 16-bit
words, so their blocks carry `bpw:16` for correct parsing and write-back.
## Bit order and planes
Two questions decide how the bits turn into pixels.
**Where do bits start?** `brev:1` means **MSB is the leftmost pixel** — the
usual arrangement for NES, Game Boy, TMS9918, VCS, and most tile-based
hardware. Without it, the LSB is leftmost (Apple II HGR, some bitmap
formats).
**How are colors decided?** Most formats use one of these two:
- **`bpp:N`** — the bits of one pixel are adjacent within a word. `bpp:4`
packs two 16-color pixels per byte.
- **`np:N`** — the bits of one pixel live in N separate words, `pofs`
apart. Plane `p` supplies bits `p*bpp`.
Game Boy 2bpp is `bpp:1,np:2,pofs:1,sl:2` — planes interleaved every
other byte. NES 2bpp is `bpp:1,np:2,pofs:8` — plane 0 in bytes 0–7,
plane 1 in bytes 8–15.
## The address model
Everything except the special modes below is described by one addressing
formula. Read it through once and the rest of the fields will make sense.
```
bpw = bpw ?? 8 // bits per word
sl = sl ?? ceil(w * bpp / bpw) // words per scanline
wpimg = wpimg ?? sl * h // words per image
pofs = pofs ?? sl * h * count // distance between bitplanes
rowstride = sl // (see il: below)
for each image n, for each row y:
yp = flip ? h-1-y : y
ofs0 = n*wpimg + yp*rowstride
shift = 0
for each column x:
ofs = remap(ofs0) // bit permutation
for each plane p:
word = data[ofs + p*pofs + skip]
bits = brev ? word >> (bpw-shift-bpp) : word >> shift
color |= (bits & ((1<<bpp)-1)) << (p*bpp)
shift += bpp
if shift >= bpw: // move to next word in the row
ofs0 += 1
shift = 0
```
Four consequences worth internalizing:
1. **`remap` is applied to `ofs0` inside the `x` loop**, after the
byte-advance. It therefore sees the byte index *within* a row as well
as the row and image index — which is exactly what lets you describe
multi-tile-wide sprites.
2. **`skip` is added last**, after `remap` and after the plane offset. It
offsets the whole block, not each image.
3. **`pofs` defaults to "one plane block after another"**
(`sl*h*count`). Set it explicitly for anything interleaved.
4. Total colors per pixel = `1 << (bpp * np)`.
### `il` — row interleaving
`il:1` stores all `count` images as one wide block, row by row: `wpimg`
becomes `sl`, and `rowstride` becomes `sl*count`. Useful for character
sets stored as a single wide strip.
## `remap` — matching hardware tile layouts
`remap` permutes the **bits of the word offset**.
It is useful for handling bitmaps in a
different order than row-major, or for rearranging sprite tiles.
Entry `i` of the array says which destination bit source bit `i` becomes:
```
remap(ofs) = OR over i of: bit i of ofs → bit remap[i] of result
```
A **negative** entry `-n` maps source bit `i` to destination bit `n-1`
**and inverts it** — use it when the hardware stores the halves in the
opposite order.
This exists because the natural row-major offset (`n*wpimg + y*sl +
xbyte`) rarely matches how hardware stores multi-tile sprites. Writing
the two offsets side by side in binary tells you the array.
### Worked example: NES 16×16 metasprite
From `presets/nes/shoot2.c`:
```c
/*{w:16,h:16,bpp:1,count:16,brev:1,np:2,pofs:8,remap:[5,0,1,2,4,6,7,8,9,10,11,12]}*/
```
`sl` = 2, `wpimg` = 32, so the natural offset is `32n + 2y + xb`:
| Source bit | Meaning | → | Dest bit | Contributes |
| --- | --- | --- | --- | --- |
| 0 | `xb` (left/right byte) | → | 5 | 32 |
| 1,2,3 | `y0,y1,y2` | → | 0,1,2 | `y & 7` |
| 4 | `y3` (top/bottom half) | → | 4 | 16 |
| 5+ | image index `n` | → | 6+ | 64·n |
Physical offset = `64n + 32·xb + 16·y3 + (y&7)`, plus `p*8` for the
plane. That is exactly NES CHR: four 16-byte tiles per metasprite in
8×16 sprite order — upper-left, lower-left, upper-right, lower-right.
### Worked example: Game Boy 16×16 metasprite
From `presets/gb/pakupaku.c`:
```c
/*{w:16,h:16,bpp:1,count:19,brev:1,np:2,pofs:1,sl:2,wpimg:64,remap:[5,1,2,3,4,0,6,7,8,9,10,11,12]}*/
```
Game Boy interleaves its planes (`pofs:1`), so `sl:2` is already consumed
by the two planes of a single 8-pixel row, and the natural offset `64n +
2y + xb` puts `xb` in bit 0 and `y` in bits 1–4. Bit 5 is never set. The
remap sends bit 0 (the 8×16 column) up to bit 5 and folds the unused bit
5 down to 0, producing `64n + 32·xb + 16·y3 + 2·(y&7)` — the L-top,
L-bot, R-top, R-bot tile order the game's OAM code uses.
### `remap` must be a bijection
In other words, if you do it in reverse, you get the original order back.
The size check computes the required data length as `max over n,i of
remap(n*wpimg + i)` for `i` in `[0, wpimg)`, plus one. If your remap
isn't a permutation of that range, the maximum comes out wrong and you
get a spurious *"Expected N values, found M"* error. This is why the
Game Boy example above bothers to map the unused bit 5 down to bit 0
instead of leaving it alone — the mapping is never exercised during
decoding, but it keeps the range exact.
### Other patterns in the tree
| Format | Meaning |
| --- | --- |
| `remap:[0,1,2,4,5,6,7,8,9,10,11,12]` | NES 8×8: doubles the image stride to 16 bytes so the two 8-byte planes fit |
| `remap:[4,0,1,2,3,5,6,7,8,9]` | TMS9918 16×16 sprite: UL, LL, UR, LR (ColecoVision, MSX) |
| `remap:[-5,0,1,2,3,5,6,7,8,9]` | Same, with the halves stored right-first |
| `remap:[3,0,1,2,4,5,6,7,8,9,10]` | Galaxian/Scramble 16×16 sprite |
## `reindex`
`reindex` throws out the shift/advance logic and replaces it with a plain
per-column table. For column `x`, `reindex[x % len]` is a **bit index
within the row**: `>> 3` gives the word offset added to the row base, `&
7` gives the bit position (still subject to `brev`). Entry `15` therefore
means word 1, bit 7. Because it drives the offset directly, the automatic
`ofs0 += 1` advance is disabled.
It exists for hardware where pixel order within a row is not linear —
notably VCS playfields, where the three playfield registers are not all
in the same bit order:
```asm
;;{w:20,h:10,flip:1,reindex:[4,5,6,7,15,14,13,12,11,10,9,8,16,17,18,19]};;
```
## Special modes
### `comp:"rletag"`
The block is RLE-compressed: the first byte is the tag, a literal byte
sets the current value, and a tag byte is followed by a repeat count
(count 0 = end). Decompression happens before decoding, so the editor
shows the expanded image. Currently, these asset blocks are view-only.
### `map:"nesnt"`
The block is an NES nametable, not a bitmap. `w`/`h` are in **tiles**
(default 32×30) and the block is rendered as a tilemap using the project's
CHR data, with a map editor instead of a pixel editor. Usually paired with
`comp:"rletag"`:
```c
/*{w:32,h:30,bpp:8,comp:"rletag",map:"nesnt"}*/
```
Currently, these asset blocks are view-only.
### `pacstrip:1`
Pac-Man / Namco arcade layout: 8-byte vertical strips of 4 rows × 8
columns, 2bpp packed as bit `y` plus bit `y+4`, X/Y mirrored within each
strip. Tiles (8×8) use 2 strips; sprites (16×16) use 8 strips in hardware
order. The mode implies 4 colors regardless of `bpp`.
```c
/*{w:8,h:8,count:256,bpp:2,pacstrip:1}*/
/*{w:16,h:16,count:64,bpp:2,pacstrip:1}*/
```
### `art:1`
Apple II HGR artifact color: bit 7 of each byte selects the color set, and
the editor shows a per-group palette toggle. The artifact bit is bit 0
when `brev` is set, bit 7 otherwise.
## Validation
Before it creates an editor, the Asset Editor counts the parsed words
(these are bytes when `bpw` is 8) and checks them against the format:
```
required = max(remap(n*wpimg + i)) + planeExtent + 1 + skip
planeExtent = (np-1)*pofs, or 0 if that is smaller than wpimg
```
The `planeExtent` rule distinguishes the two plane layouts: when planes
are interleaved (`pofs < wpimg`, Game Boy/SMS) the plane bytes are already
inside each image block; when planes are stored as separate blocks
(`pofs >= wpimg`, NES) the extra plane is counted at the end.
A mismatch shows as **"Expected N value(s), found M"** in the Asset
Editor tab. When that happens, check things in this order:
1. **`found 0`** — the literals have no radix prefix, or the data block
ended early at a stray `;`.
2. `count` — the most common culprit otherwise.
3. `wpimg` — hardware often pads (C64 sprites are 24×21 bits in 64
bytes).
4. `remap` — is it a bijection over `[0, wpimg)`?
5. `pofs` — interleaved planes need it set explicitly.
## Cookbook
Real formats to copy from the preset tree:
| Platform | Header |
| --- | --- |
| NES 8×8 CHR | `/*{w:8,h:8,bpp:1,count:256,brev:1,np:2,pofs:8,remap:[0,1,2,4,5,6,7,8,9,10,11,12]}*/` |
| NES 16×16 metasprite | `/*{w:16,h:16,bpp:1,count:16,brev:1,np:2,pofs:8,remap:[5,0,1,2,4,6,7,8,9,10,11,12]}*/` |
| NES palette | `/*{pal:"nes",layout:"nes"}*/` |
| NES nametable | `/*{w:32,h:30,bpp:8,comp:"rletag",map:"nesnt"}*/` |
| Game Boy 8×8 tile | `/*{w:8,h:8,bpp:1,count:4,brev:1,np:2,pofs:1,sl:2}*/` |
| Game Boy 16×16 metasprite | `/*{w:16,h:16,bpp:1,count:19,brev:1,np:2,pofs:1,sl:2,wpimg:64,remap:[5,1,2,3,4,0,6,7,8,9,10,11,12]}*/` |
| ColecoVision / MSX sprite | `/*{w:16,h:16,brev:1,remap:[4,0,1,2,3,5,6,7,8,9],count:2}*/` |
| ColecoVision layered 2-plane sprite | `/*{w:16,h:16,remap:[-5,0,1,2,3,5,6,7,8,9],count:5,np:2}*/` |
| C64 hires sprite | `/*{w:24,h:21,bpp:1,brev:1,wpimg:64,aspect:1,count:3}*/` |
| C64 multicolor sprite | `/*{w:12,h:21,bpp:2,brev:1,wpimg:64,count:4,aspect:2}*/` |
| VCS sprite | `;;{w:8,h:16,brev:1,flip:1};;` |
| VCS palette | `;;{pal:"vcs"};;` |
| Galaxian/Scramble tile ROM | `/*{w:16,h:16,remap:[3,0,1,2,4,5,6,7,8,9,10],brev:1,np:2,pofs:2048,count:64}*/` |
| Pac-Man tiles / sprites | `/*{w:8,h:8,count:256,bpp:2,pacstrip:1}*/` · `/*{w:16,h:16,count:64,bpp:2,pacstrip:1}*/` |
| Pac-Man color PROM | `/*{pal:"pacman",n:32}*/` |
| Astrocade | `/*{w:16,h:16,bpp:2,brev:1}*/` · `/*{pal:"astrocade",layout:"astrocade"}*/` |
| Williams | `/*{w:16,h:16,bpp:4,brev:1}*/` |
| PC Engine 16×16 sprite | `/*{w:16,h:16,bpp:1,count:3,brev:1,np:4,pofs:16,sl:1,bpw:16,wpimg:64}*/` |
| Apple II HGR | `/*{w:8,h:8,bpp:1,count:96}*/` (LSB-first, no `brev`) |
| Verilog 16-bit words | `/*{w:16,h:16,bpw:16,count:5}*/` |
+69
View File
@@ -0,0 +1,69 @@
# Asset Editor
The Asset Editor finds [Asset Headers](asset-headers.md) in your source — small JSON
descriptors inside comments — and turns the data block that follows into
an editable bitmap, tilemap, or palette. Edits are written straight back
into the source text, preserving the original number base and formatting
of each literal.
## Asset List
Open the **Asset Editor** from the sidebar to see every editable asset in
your project in one scrolling list. Assets are grouped by file — a heading
with the filename, then each asset underneath, in the order it appears in
the source.
Each asset shows a **line number** you can click to jump to that spot in
the code (and highlight it), the [Asset Header](asset-headers.md) as
written in the file, and the **editor** itself — a picture, a palette, or
a tilemap. If something is wrong with an asset, a red error message
appears in place of its editor.
Click an asset to start editing; the editor scrolls into view, and your
selection is saved in the address bar so you can bookmark or share it.
You don't need to understand the header format to edit graphics, but if
you want to add or change one, [Asset Headers](asset-headers.md) explains
it.
### Editing images
Assets with a width and height show a **grid of thumbnails** — one per
image. Click the one you want, and a large editor opens beside it.
To paint, just click and drag on the image. Pick a color from the row of
swatches below it (each labelled with its palette index). Starting a drag
on a pixel that's already the color you picked will erase instead of
paint.
The toolbar above the image has quick transforms: **flip** it
horizontally or vertically, **rotate** it, **nudge** it in any direction,
and **copy/paste** it. Copy and paste work between any two images in the
session, and they keep the palette indices intact, so a tile copied from
one asset still looks right in another even if the colors differ.
If more than one palette in your project matches the image's color count,
a dropdown appears under the thumbnails so you can try them out — the
previews update instantly.
In Apple II artifact-color mode (`art:1`), the artifact column is edited
separately and is left alone by the transforms.
### Editing palettes
Palettes are edited one color at a time. The palette appears as a small
table; click any cell to open a color picker. Small, indexed palettes show
a grid of swatches to choose from — hover over one to see its value and
hex color. Larger, direct-color palettes get color sliders instead. As you
change a color, the source code is updated right away.
### Editing tilemaps
NES nametable assets (`map:"nesnt"`) show the whole tilemap composited
from your project's graphics and palette, as a single preview.
### Undo and redo
Use `Ctrl/Cmd+Z` to undo and `Ctrl/Cmd+Shift+Z` to redo your edits. The
history starts fresh each time you open the Asset Editor, so undo only
affects the changes you've made in this session.
+66
View File
@@ -0,0 +1,66 @@
# Breakpoints
The Breakpoints window lists every breakpoint in the project and lets you
add, edit, enable, or delete them. It is available on platforms that
implement `runEval` or `runToPC`.
## Kinds of breakpoints
| Kind | Created by | Identified by |
| --- | --- | --- |
| **Source** | Clicking a line in the editor gutter, or **Run To Line** | file and line number |
| **Address** | The form in this window | a symbol, `$hex`, or decimal address |
Source breakpoints are shown as red circles in the editor's gutter.
Address breakpoints are resolved against the debug symbol map, so a symbol from
a linker map works as well as a raw address.
A breakpoint that cannot be
resolved right now (for example a symbol that does not exist for the
current platform) is shown with an error instead of an address.
## Conditions
Any breakpoint can carry a condition expression. The breakpoint only
stops when the expression evaluates to a non-zero value. Conditions are
evaluated against CPU state while the emulator is running.
Supported values:
| Value | Example |
| --- | --- |
| Decimal number | `32` |
| Hex number | `$1a2f`, `0x1a2f` |
| CPU register | `PC`, `A`, `X`, `Y`, `SP`, &hellip; |
| Debug symbol | `mainloop` |
| Memory byte | `[$0200]` |
Supported operators, in C-like precedence: `!` `~` `-` `+` (unary),
`*` `/` `%`, `+` `-`, `<<` `>>`, `<` `<=` `>` `>=`, `==` `!=`, `&`,
`^`, `|`, `&&`, `||`, and parentheses. The whole expression is true
when it evaluates to a non-zero value.
For example, `A == $20 && X < 4` stops only when the accumulator holds
`$20` and the X register is less than 4.
Conditions are parsed to a small closure — never `eval()` — so an
invalid expression fails safely.
## Managing breakpoints
- **Add** — type an address or symbol and an optional condition, then
click **Add**. Adding the same address again updates that breakpoint
instead of creating a duplicate.
- **Enable/disable** — toggle the checkbox on the left. Disabled
breakpoints are dimmed.
- **Edit** — click the pencil to load a breakpoint into the form;
**Save** applies the change.
- **Delete** — click the trash icon.
- **Jump** — click a breakpoint's location to open the source file, or
the disassembly, at the breakpoint's address.
The breakpoint that the CPU is currently stopped at is highlighted.
Breakpoints are persisted in browser local storage, scoped to the
current platform and project file, so they survive a reload.
+228
View File
@@ -0,0 +1,228 @@
# Build Directives
A project can contain multiple files. The build system decides how each
file is used based on directives in your source, and each file's
extension selects the tool that compiles or assembles it.
## Projects with multiple files
All files in a project must be referenced from the main project file with
one of these reference types:
- **Include file** — a source file embedded in another using an
`#include` or similar directive.
- **Linked file** — a source file compiled/assembled and linked into the
project alongside the main file, using the `//#link` or `;#link`
directive. A binary file can be included in an assembly project with the
`incbin` directive.
- **Resource file** — a file that must be included in the project but is
neither included nor linked from the main file (for example it is
included from a non-main file). Use the `//#resource` or `;#resource`
directive.
Here's a C example:
```c
//#link "lzg.c"
#include "lzg.h"
```
If a file cannot be found, check the syntax. Often double quotes are
required, e.g. `#include "file.inc"`.
Only system files that are part of the compiler toolchain can be found
with the `<foo.h>` syntax.
Assembler files can include files using the `.include` or `.incbin`
directives:
```
.segment "CHARS"
.incbin "tileset.chr"
```
## C preprocessor defines
The preprocessor macro `__MAIN__` is defined when compiling the main C
file of a project. You can use this to run tests on your module files:
```c
//#link "file_needed_for_test.c"
#ifdef __MAIN__
#include "file_needed_for_test.h"
void main() {
test_my_module();
}
#endif
```
The macro `__8BITWORKSHOP__` is defined on all C files.
## Build directives
Build directives are comment lines that tell the build system how to
compile, assemble, or link a project. They can appear in any source file
and are never seen by the toolchain:
- C, SDCC, and cmoc sources mark them with `//`.
- Assembly sources mark them with `;`.
The keyword follows the comment marker and a `#`, and the line must
begin with the comment (leading whitespace is allowed).
### `//#symbol` — define a symbol
Defines a preprocessor macro, an assembler symbol, or a linker symbol:
~~~c
//#symbol [<phase>] NAME[=VALUE]
~~~
The optional *phase* is one of:
| Phase | Meaning |
| --- | --- |
| `c`, `cc`, `compiler` | C preprocessor define (default) |
| `as`, `asm`, `assembler` | assembler symbol |
| `ld`, `link`, `linker` | linker symbol |
A compiler define takes a text value (`cc65 -D`, `sdcc -D`). Assembler
and linker symbols take an integer expression:
~~~c
//#symbol VERSION=3
//#symbol as START=$8000
//#symbol ld NES_MAPPER=4
~~~
A linker symbol defined by a source file replaces (rather than
duplicates) a symbol of the same name that the platform already defines,
so `//#symbol ld NES_MAPPER=...` can select a different mapper without
editing the platform defaults. Linker values must be integer
expressions; a text value is reported as a build error rather than
passed to the linker.
### `//#flag` — pass a raw argument
Passes arguments straight to one tool phase, with no interpretation:
~~~c
//#flag <phase> <arg> [<arg> ...]
~~~
~~~c
//#flag c -Osir
//#flag as --cpu 65c02
//#flag ld -m map.txt
~~~
Arguments may be quoted with single or double quotes when they contain
spaces. This is an escape hatch that the build system cannot validate.
### `//#tooldef` — set a typed build parameter
Sets a named build parameter that the build system understands:
~~~c
//#tooldef <phase> NAME=VALUE
~~~
For the linker phase, the supported parameters are:
| Name | Meaning |
| --- | --- |
| `cfgfile` | linker configuration file to use (e.g. for CC65's ld65) |
| `libargs` | comma-separated list of library/symbol arguments |
~~~c
//#tooldef ld cfgfile=apple2-hgr2.cfg
//#tooldef ld libargs=,nes.lib
~~~
### Commenting out a directive
To disable a directive, break the marker; for example, use `////#` in C
or `;;#` in assembly. A directive that is already commented out is
deliberately *not* recognized.
### Legacy `#define` directives
For backwards compatibility the build system still understands the older
`#define`-based forms:
| Legacy form | Same as |
| --- | --- |
| `#define CFGFILE <file>` | `//#tooldef ld cfgfile=<file>` |
| `#define LIBARGS <a,b,...>` | `//#tooldef ld libargs=<a,b,...>` |
| `#define NES_MAPPER <n>` | `//#symbol ld NES_MAPPER=<n>` |
| `#define CC65_FLAGS <a,b,...>` | `//#flag c <a> <b> ...` |
These may be prefixed with `;` or `/` as well (for example `;#define`),
so they can appear in assembly sources. New code should prefer the
explicit `//#` directives above.
## CC65 custom config files
CC65's linker is controlled with
[configuration files](https://www.cc65.org/doc/ld65-5.html) that define
memory areas and the layout of the final binary or ROM.
Tell the linker to use a custom config file from your main C program:
~~~c
//#tooldef ld cfgfile=apple2-hgr2.cfg
//#resource "apple2-hgr2.cfg"
~~~
The legacy form is also supported:
~~~c
#define CFGFILE apple2-hgr2.cfg
//#resource "apple2-hgr2.cfg"
~~~
Note the lack of quotes around the file name.
## CC65 `#embed` directive
You can include binary files directly in
CC65 `.c` files, a feature not available from the CC65 command line. For
example, given an uploaded file `image-c64.multi.lz4`:
~~~c
const char image_c64_multi_lz4[] = {
#embed "image-c64.multi.lz4"
};
~~~
This creates a byte array named `image_c64_multi_lz4` with the contents
of the binary file.
The `#embed` directive is also supported in Oscar64 and Wiz files.
## SDCC `#pragma`
With SDCC, enable additional optimization by adding one of these lines to
the top of the file:
~~~c
#pragma opt_code_size // for smaller code
#pragma opt_code_speed // for faster code
~~~
Either pragma slows builds but produces faster and smaller code.
## File extensions
The extension of your source file determines the tool used to assemble
or compile it. Common extensions:
| Extension | File type | Tool |
| --- | --- | --- |
| `.c` | C source file | cc65, sdcc, cmoc |
| `.s` `.ca65` | Assembler file | ca65 |
| `.a` | Assembler file | dasm |
| `.zmac` | Assembler file | zmac (Z80) |
| `.v` | Verilog file | Verilator |
| `.bb` `.bas` | batariBASIC file | batariBASIC (vcs) |
| `.fb` `.bas` | FastBASIC file | FastBASIC (atari8) |
+18
View File
@@ -0,0 +1,18 @@
# Call Stack
The Call Stack window reconstructs the program's call graph from runtime
stack activity. It is available on platforms that implement
`startProbing`.
Each node represents a routine, and its children are the routines it
called. A node shows how many times it was called and (while expanded)
the scanline range where it ran. Because the tree is built from stack
pushes and pops plus the observed jump distances, it reflects what the
program actually did rather than what the source says.
Counts are cumulative: the tree keeps growing as the program runs. The
probe buffer is cleared on each refresh, and the graph resets when the
emulator is reset.
Expanding a node reveals its callees, so you can drill into a hot
routine and see where the time and calls are going.
+29
View File
@@ -0,0 +1,29 @@
# CRT Probe
The CRT Probe shows memory and I/O activity laid out by *raster
position* instead of by address, so you can see what the CPU touches at
each point of the display sweep. It is available on platforms that
implement `startProbing`.
The canvas axes are:
- **X** is the cycle within the scanline.
- **Y** is the scanline.
Activity is color-coded:
| Color | Operation |
| --- | --- |
| Grey ramp | Execute (shade reflects stack depth, so call depth is visible) |
| Blue | Memory write |
| Cyan | VRAM write |
| Light blue | I/O write |
| Green | I/O read |
| Green fill | Wait |
| Pink | Interrupt |
Illegal operations and DMA reads are drawn in grey. Gaps in the trace
are filled with the previous color, so the display does not show false
black bands.
The view shows a complete frame, and might be incomplete during debugging.
+23
View File
@@ -0,0 +1,23 @@
# Debug Tree
The Debug Tree shows a hierarchical view of platform debug information.
It is available on platforms that implement `getDebugTree`.
The tree's shape is defined by the platform. Where the
[Call Stack](callstack.md) reconstructs call flow from observed
execution, the Debug Tree exposes whatever structured state the machine
implements — device registers, internal tables, subsystem state — as an
expandable tree.
## How values are shown
- Numbers are shown in decimal with their hex value.
- Booleans and strings are shown as-is.
- Byte arrays are shown as a short hex dump.
- Objects and functions expand to their properties. Functions marked as
lazy are only called when the node is expanded.
- Large arrays are split into `$offset` chunks on demand.
- Maps are shown as their key/value pairs.
Nodes collapse and expand on click. The tree refreshes with the emulator
while debugging.
+35
View File
@@ -0,0 +1,35 @@
# Disassembly
The Disassembly window disassembles machine code around the current
program counter. It is available on platforms that expose both a
disassembler and a savable CPU state.
Each line shows:
| Column | Meaning |
| --- | --- |
| Address | The 16-bit program counter for the instruction |
| Bytes | The raw opcode bytes (padded for alignment) |
| Mnemonic | The disassembled instruction |
| Comment | The symbol defined at that address, if any |
The view is centered on the program counter (PC). While debugging, it
follows the PC as you step. Symbol names are substituted for addresses
when a symbol map is available, so jumps and loads show labels instead
of raw hex.
## Navigating
- **Go To Address** re-centers the window on an address or symbol and
highlights it.
- Addresses in the operand columns are linked to their symbols when the
map contains them.
## Listing vs. disassembly
The Disassembly window is generated live by the emulator's disassembler.
A compiler or assembler *listing* is a separate, read-only window built
from the tool's own output; see [Editor](editor.md). The listing follows
the program counter using the line-to-address map from the assembler,
which is more accurate for source-level navigation than the live
disassembly.
+53
View File
@@ -0,0 +1,53 @@
# Editor
The source editor is based on [CodeMirror 6](https://codemirror.net/).
Syntax highlighting is chosen from the toolchain that handles the file's
extension, so a `.c` file opens as C, a `.ca65` file as 6502 assembly,
and so on. Every edit triggers a rebuild; see the
[IDE overview](index.md).
## File windows
The sidebar lists one entry per file in the project:
- **Source files** — the main file and any file reached by an
`#include` or `//#link` directive. These open in the editable source
editor. See [Build Directives](build-directives.md).
- **Listings** — compiler/assembler listings (for example `.lst` or
symbol listings) generated by the build. These open read-only. The
view follows the program counter while debugging and supports **Go To
Address**.
- **Toolchain headers** — clicking the badge next to an `#include` line
opens the referenced header from the toolchain's bundled include
filesystem in a read-only viewer.
- **Binary files** — binary files open in a read-only hex dump.
## Gutter
For some platforms, the gutter shows additional information next to source lines:
- **Hex offset** -- The offset of a given source line.
- **Code bytes** -- The bytes for a given assembly instruction.
- **CPU Cycles** -- The number of CPU cycles for a given assembly instruction.
- Click the hex offset (or the yellow arrow) to run and break on that line.
- Click the red circle to toggle a breakpoint — see [Breakpoints](breakpoints.md).
## Editor keyboard shortcuts
### Editing
| Shortcut | Action |
| --- | --- |
| `Tab` | Insert a tab or indent the selected line(s) |
| `Shift+Tab` | Outdent the line(s) or selected range(s) |
| `mod+Z` | Undo |
| `mod+Shift+Z` | Redo |
| `mod+F` | Find |
| `mod+G` | Find next match |
| `mod+D` | Select the next occurrence of the selection |
| `mod+Shift+Backspace` | Delete the current line |
The editor also inherits the standard [CodeMirror 6](https://codemirror.net/) keymap,
including cursor/word movement and selection with the arrow keys, and cut/
copy/paste (`mod+X`, `mod+C`, `mod+V`).
+29
View File
@@ -0,0 +1,29 @@
## Embedding the IDE in a web page
You can embed the IDE using an IFRAME element:
```html
<iframe src="https://8bitworkshop.com/redir.html?embed=1&..."></iframe>
```
Query-string parameters:
| Name | Value |
| --- | --- |
| `embed` | must be `1` |
| `platform` | platform ID (for example `vcs`) |
| `file0_name` | name of the main file |
| `file0_data` | text content of the main file |
| `highlight` | (optional) line range to highlight (for example `2,4`) |
The IDE uses browser storage tied to the referrer URL, so each page
containing embeds is effectively sandboxed from the others.
Example using Bootstrap's `embed-responsive` class:
```html
<div class="embed-responsive embed-responsive-16by9">
<iframe class="embed-responsive-item" loading="lazy"
src="https://8bitworkshop.com/redir.html?embed=1&platform=nes&highlight=&file0_name=testembed.dasm&file0_data=..."></iframe>
</div>
```
+132
View File
@@ -0,0 +1,132 @@
# 8bitworkshop IDE
The 8bitworkshop IDE is a browser-based development environment for
micro computers and consoles.
The IDE compiles or assembles your source code
while you type, then the emulator runs your code.
This happens all locally in your web browser.
## Choosing a platform
Switch between hardware platforms using the green **Platform Selector**
to the right of the main menu.
Each platform has its own virtual file system in the browser, so edits and
new files are only visible to the current platform.
## Example programs
The IDE ships with example programs for every platform. Use the
**Project Selector** drop-down (next to the menu icon) to load one.
Edits are persisted in browser local storage and survive closing the
tab. To restore an example to its original state, choose
**File &raquo; Revert to Original&hellip;**. To start from scratch, choose
**New Project&hellip;** and enter a filename (typically `.c` for C, or
`.asm` for assembly).
See [Managing Files](managing-files.md) for how to
export and import files, share playable links and videos, sync projects to GitHub, and pull projects from GitHub.
## Emulators
Every platform includes a built-in emulator that runs your code on
simulated hardware. Click the emulator screen to give it keyboard focus.
The IDE shows the key bindings used by the current platform; some
platforms also support gamepads.
## Compilers
C compilers and assemblers for most platforms run in the browser. Each
time you change the code it is recompiled automatically. Errors appear
in a list in the upper-right corner of the page; click the link beside
an error to jump to the offending source line.
If the build succeeds, the emulator restarts with the new ROM image, so
you see changes near-instantly.
See [Build Directives](build-directives.md) for information on
multi-file projects, compiler/linker directives, and file extensions.
## Debugger
The IDE includes a debugger for stepping through machine code, viewing
memory, and starting/stopping the program. The buttons at the top of the
screen perform these functions:
- **Reset:** hard-reset the emulator, then single-step to the first
instruction.
- **Pause:** stop the emulator.
- **Run:** resume the emulator after pausing.
- **Single Step:** execute the next CPU instruction.
- **Step Over:** execute until the next source line, or step over a subroutine, then break.
- **Next Frame/Interrupt:** run until the next video frame starts, then
break.
- **Run To Line:** set a breakpoint on the current source line (or click
the gutter to the left of a line). The emulator stops when execution
reaches that instruction.
- **Step Out of Subroutine:** run until the current subroutine returns,
then break.
- **Step Backwards:** step back a single CPU instruction.
Some platforms also offer:
- **Analyze Timing:** perform a *flow analysis* of your code and compute
timing values for each instruction.
- **Highlight Executed Lines:** highlight lines of source code as they are executed by the emulator.
- **Start Recording:** enable the replay feature, which lets you rewind
the emulator and scroll to arbitrary frames and CPU cycles.
When the IDE hits a breakpoint or a single-step, a debug window appears
in the lower-right showing the CPU state. Click the links at the bottom
of the window for more detail. See [Breakpoints](breakpoints.md).
## Sidebar windows
The sidebar to the left of the editor lists every source file in your
project — the main file (shown in the pulldown), any included or linked
files, and any generated listings. Depending on the platform, additional
tools appear:
| Window | Description |
| --- | --- |
| [Editor](editor.md) | Edit source files; read-only listings, headers, and binary views |
| [Disassembly](disasm.md) | Disassemble the program around the program counter |
| [Memory Browser](memory.md) | Hex dump of CPU memory |
| [Memory Map](memmap.md) | Map of linker segments and memory areas |
| [VRAM Browser](vram.md) | Hex dump of video memory (systems with a separate VDP) |
| [Memory Probe](memprobe.md) | Heat map of memory read/write activity |
| [CRT Probe](crtprobe.md) | Memory activity laid out by raster position |
| [Probe Log](probelog.md) | Textual log of CPU/memory activity |
| [Scanline I/O](scanlineio.md) | I/O and VRAM access per scanline |
| [Symbol Profiler](symbols.md) | Read/write counts per symbol |
| [Call Stack](callstack.md) | Call graph reconstructed from the stack |
| [Debug Tree](debugtree.md) | Hierarchical view of platform debug info |
| [Breakpoints](breakpoints.md) | Add and edit breakpoints |
| [Asset Editor](asseteditor.md) | Edit bitmaps, tilemaps, and palettes embedded in source |
## Keyboard Shortcuts
`mod` below is **Ctrl** on Windows/Linux and **Cmd** on macOS.
| Shortcut | Action |
| --- | --- |
| `F1` | Help for the active view |
| `F8` | Pause / resume |
| `mod+Shift+F` | Search symbols, files, and docs |
| `mod+Shift+G` | Go To Address |
| `mod+Shift+R` | Reset and run |
| `mod+Shift+D` | Reset and break |
| `mod+Shift+L` | Single step |
| `mod+Shift+K` | Step over |
| `mod+Shift+I` | Step out of subroutine |
| `mod+Shift+J` | Step backwards |
| `mod+Shift+X` | Next frame/interrupt |
| `mod+Shift+A` | Restart at cursor |
| `mod+Shift+Y` | Run to line |
The context-sensitive shortcuts are also shown as clickable chips in the
status bar at the bottom of the window (toggle it under
**Settings &raquo; Show keyboard shortcuts / status bar**).
See [Editor](editor.md) for editor-specific shortcuts.
+91
View File
@@ -0,0 +1,91 @@
# Managing Files
## Importing and exporting files
If you plan to clear cookies, or move between browsers or computers, you
may need to export your work and import it later.
To save your files outside of browser cache:
1. Select **Download** from the menu.
2. Choose an option:
- **Download Source File** — download the active source file.
- **Download ROM Image** — download the compiled ROM, which can be
used in a standalone emulator.
- **Download Project as Zip** — download the active project as a zip.
- **Download All Changes as Zip** — download all files in the active
platform.
3. Choose a location and save the file.
To import source files back into the browser, select **Upload** from the
menu, choose the file, and click **Open**.
## Sharing projects
You can share playable links and videos (seven-second animated GIFs) with
others. Playable links contain the project code inside the URL, so you
can share your work without storing it remotely. Some browsers may not
support the resulting URL length.
### Sharing a playable link
1. From the menu, select **Share**.
2. Select **Share Playable Link…**.
3. Select **Copy Direct Link** to copy the link, or **Copy IFRAME Tag**
to copy an embeddable IFRAME snippet.
4. Click **Close**.
### Recording a video
1. From the menu, select **Share**.
2. Select **Record Video…**. The emulator background turns red while
recording.
3. When the preview appears, right-click it and choose **Save Image
As…** to save the animated GIF.
4. Click **Close**.
## Syncing with GitHub
You can import projects from GitHub repositories, publish your projects,
and push/pull to repositories you have access to. When you publish or
push, the IDE uploads your code and a compiled ROM file.
### Connecting your account
1. From the menu, select **Sync &raquo; Sign in to GitHub…**.
2. Enter your GitHub username and password.
### Importing a project
1. From the menu, select **Sync**, then **Import Project from
GitHub…**.
2. Enter the repository URL (for example
`https://github.com/username/reponame`) and click **Import Project**.
To leave the project, select **Leave Repository** from the Project
Selector drop-down. Imported repositories appear under **Repositories**
in the same drop-down.
### Deleting a local repository
1. Select the repository from the Project Selector drop-down.
2. Select **Sync &raquo; Delete Local Repository**.
3. Type **YES** to confirm, then select **OK**.
### Publishing a project
1. From the menu, select **Sync &raquo; Publish Project on GitHub…**.
2. Enter a project name and description.
3. Choose **Public** or **Private** visibility.
4. Select a license.
5. Click **Upload Project**. If you aren't signed in you'll be prompted
to connect first.
### Pushing and pulling changes
- **Push:** select **Sync &raquo; Push Changes to Repository…**, enter a
commit message, then **Push Changes**.
- **Pull:** select **Sync &raquo; Pull Latest from Repository**, then
**OK**. All local files are overwritten; there is no merge in the
browser.
+24
View File
@@ -0,0 +1,24 @@
# Memory Map
The Memory Map draws the memory image produced by the linker: each
segment is a bar whose position is its start address, whose height is
proportional to its size, and whose color reflects its type.
The window appears when the current project has segment information,
which most linker-based toolchains provide.
- Hover a segment to see a tooltip with its start and end addresses and
its size in bytes.
- Free space between segments is drawn as an empty bar.
- Segments without a concrete address come from the linker rather than
an explicit memory area.
## Navigating
Click any segment to open the [Memory Browser](memory.md) and
scroll it to that segment's start address.
## Notes
- Overlapping segments (for example ROM and a load segment) currently
share the same row; the map does not yet stack them.
+22
View File
@@ -0,0 +1,22 @@
# Memory Browser
The Memory Browser shows a hex dump of CPU memory. It is available on
platforms that implement `readAddress`.
The dump is laid out 16 bytes per row:
```
0000 00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e 0f
```
- Each row begins with the address of its first byte.
- Rows are color-coded by the memory segment they fall into, using the
same segment data as the [Memory Map](memmap.md).
- When the window opens it scrolls to the program's data segment.
## Navigating
- **Go To Address** scrolls to an address or symbol and highlights the
row containing it.
- Clicking a segment in the [Memory Map](memmap.md) opens this
window and scrolls to that segment's start address.
+35
View File
@@ -0,0 +1,35 @@
# Memory Probe
The Memory Probe is a heat map of memory activity. It is available on
platforms that implement `startProbing`.
The canvas is a 256&times;256 grid of the 64K address space:
- **X** is the low byte of the address.
- **Y** is the high byte.
- The faint background shows a compressed view of current memory
contents, so the map is still readable before anything executes.
Activity is drawn in color by operation type:
| Color | Operation |
| --- | --- |
| Green | Execute |
| Red | Memory read |
| Blue | Memory write |
| Teal | I/O read |
| Magenta | I/O write |
| Yellow | DMA/VRAM read |
| Light blue | DMA/VRAM write |
| Bright green | Interrupt |
| White | Illegal / error |
| Pink | Wait |
The view shows a complete frame, and might be incomplete during debugging.
## Tooltips
Hovering the map shows the routines and operations that touched that
address, including the program counter that performed each access and
any associated value.
+21
View File
@@ -0,0 +1,21 @@
# Probe Log
The Probe Log is a chronological text view of CPU and memory activity
for a frame. It is available on platforms that implement `startProbing`.
Each line describes one clock tick:
```
(row, col) <disassembled instruction> <operations...>
```
- The first pair is the scanline and cycle within the scanline.
- The instruction column shows the disassembly of the code executing at
that clock.
- The remaining columns list what the instruction did — reads, writes,
I/O and VRAM accesses, stack pushes and pops, and interrupts — with
addresses and, where relevant, values.
Write operations are colored differently from reads so they stand out.
The view shows a complete frame, and might be incomplete during debugging.
+19
View File
@@ -0,0 +1,19 @@
# Scanline I/O
The Scanline I/O window shows the I/O and VRAM accesses made during each
scanline. It is available on platforms that implement `startProbing`.
Each row is one scanline:
- A leading scanline number.
- One character column per CPU cycle. An access at that cycle is shown
as its device address in hex; cycles with no access are shown as `.`.
- The horizontal-blank boundary is marked with `|`.
- The symbol of the routine executing on that scanline is appended at the
end, when known.
This makes it easy to see where per-scanline register writes (raster
effects) occur relative to the start of the visible line.
The view shows a complete frame, and might be incomplete during debugging.
+23
View File
@@ -0,0 +1,23 @@
# Symbol Profiler
The Symbol Profiler counts memory reads and writes per symbol over time.
It is available on platforms that implement `startProbing`.
Each row lists a symbol with two counters:
| Column | Meaning |
| --- | --- |
| Symbol | The debug symbol covering that address |
| Reads | Number of memory reads at addresses belonging to the symbol |
| Writes | Number of memory writes |
Rows are color-coded:
- **Code** (green) — the symbol's address was executed.
- **I/O** (teal) — the symbol covers an I/O address.
- **Data** (blue) — the symbol was only read or written.
Counts are cumulative and the probe buffer is cleared each refresh, so
the numbers keep growing while you run and are reset when data is
collected again. Symbols from the debug map are listed even before they
are touched.
+11
View File
@@ -0,0 +1,11 @@
# VRAM Browser
The VRAM Browser is a hex dump of video memory. It is available on
systems with a separate video chip and a `readVRAMAddress` implementation.
It behaves like the [Memory Browser](memory.md) — 16 bytes per
row — but reads through the platform's video-memory accessor instead of
CPU memory, and its rows are all shown with the `video` segment color.
The dump does not scroll to a data segment on open, since VRAM has no
linker segments.
+131 -23
View File
@@ -1,15 +1,132 @@
/**
* DocsSource - searches the IDE's own documentation.
*
* The help docs are already bundled as rendered HTML strings (the `.md`
* imports in ../views/helpview.ts), so rather than fetch a prebuilt
* `searchindex.json` we derive records from those same strings at runtime.
* That keeps the docs searchable in every deploy (dev, prod, gh-pages, the
* embedded IDE) with no extra build step or artifact to keep in sync.
*
* helpview registers the topic list via setDocsProvider() during IDE startup.
* The HTML-to-records extraction below is a pure function so it can be
* exercised under plain Node (where `HELP_TOPICS`'s `.md` imports can't load).
*/
import { SymbolIndex } from "./symbolindex";
import { SymbolSourceKind, PrebuiltIndex, SYMIDX_VERSION } from "../../common/searchtypes";
import { SymbolRecord, SymbolSourceKind } from "../../common/searchtypes";
import { SearchHit, SearchSource } from "./types";
/** A help topic, mirroring HelpTopic in ../views/helpview.ts. */
export interface DocTopic {
id: string; // help topic id, e.g. 'editor'
title: string; // window/sidebar title, e.g. 'Editor'
html: string; // rendered Markdown
}
type DocsProvider = () => DocTopic[];
let docsProvider: DocsProvider = () => [];
/** Register the help-topic list. Called once during IDE startup. */
export function setDocsProvider(fn: DocsProvider) {
docsProvider = fn;
}
const HEADING_RE = /<h([1-3])\b[^>]*>([\s\S]*?)<\/h\1>/gi;
/** Decode the handful of entities markdown-it emits into plain text. */
function decodeEntities(s: string): string {
return s
.replace(/&lt;/g, '<')
.replace(/&gt;/g, '>')
.replace(/&quot;/g, '"')
.replace(/&#(?:0*39|x27);/gi, "'")
.replace(/&nbsp;/g, ' ')
.replace(/&hellip;/g, '…')
.replace(/&mdash;/g, '—')
.replace(/&ndash;/g, '–')
.replace(/&rsquo;|&lsquo;/g, "'")
.replace(/&rdquo;|&ldquo;/g, '"')
.replace(/&raquo;/g, '»')
.replace(/&laquo;/g, '«')
.replace(/&amp;/g, '&');
}
/** Strip tags/entities to a single line of searchable text. */
export function htmlToText(html: string): string {
return decodeEntities(html.replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim();
}
/** First sentence (or a leading snippet) of some prose. */
function firstSentence(text: string, max = 180): string | undefined {
const t = text.trim();
if (!t) return undefined;
const head = t.slice(0, max);
const m = head.match(/^([\s\S]*?[.!?])(?:\s|$)/);
if (m) return m[1];
return t.length <= max ? t : t.slice(0, max - 1) + '…';
}
/**
* Turn one rendered doc page into SymbolRecords: one for the page itself and
* one per section heading. The section text lands in `brief`/`detail`, which
* MiniSearch indexes for prose queries.
*/
export function extractDocRecords(topic: DocTopic): SymbolRecord[] {
const records: SymbolRecord[] = [];
const route = '#help/' + topic.id;
const file = topic.title || topic.id;
const html = topic.html || '';
const headings: { name: string; start: number; end: number }[] = [];
const re = new RegExp(HEADING_RE.source, 'gi');
let m: RegExpExecArray | null;
while ((m = re.exec(html))) {
headings.push({ name: htmlToText(m[2]), start: m.index, end: re.lastIndex });
}
const addRecord = (index: number, name: string, text: string) => {
if (!name) return;
const brief = firstSentence(text);
// Keep the whole section in `detail` so text past the first sentence
// (later table rows, lists) is still searchable via MiniSearch.
const detail = brief && text.startsWith(brief) ? text.slice(brief.length).trim() : text;
records.push({
id: `${topic.id}:${index}`,
name,
kind: 'doc',
brief,
detail: detail || undefined,
source: 'docs',
file,
url: route,
});
};
// Intro before the first heading belongs to the page record. Pages usually
// open with their <h1> at offset 0, in which case there is no intro; the
// page record still gets a name so the title itself is searchable.
const firstHeadingStart = headings.length ? headings[0].start : html.length;
const introText = htmlToText(html.slice(0, firstHeadingStart));
const pageTitle = (headings.length && headings[0].start === 0)
? (headings[0].name || topic.title) : (topic.title || topic.id);
addRecord(0, pageTitle, introText);
for (let i = 0; i < headings.length; i++) {
// Skip the page's own title heading; the page record already covers it.
if (i === 0 && headings[0].start === 0) continue;
const contentEnd = i + 1 < headings.length ? headings[i + 1].start : html.length;
addRecord(i + 1, headings[i].name, htmlToText(html.slice(headings[i].end, contentEnd)));
}
return records;
}
export class DocsSource implements SearchSource {
id: string = 'docs';
kind: SymbolSourceKind = 'docs';
private index: SymbolIndex | null = null;
private readyPromise: Promise<void> | null = null;
constructor() {}
async ready() {
if (this.readyPromise) return this.readyPromise;
this.readyPromise = this.load();
@@ -17,29 +134,20 @@ export class DocsSource implements SearchSource {
}
private async load() {
return; // TODO
try {
const response = await fetch('/docs/searchindex.json');
if (!response.ok) {
return;
}
const json = await response.json();
if (json.version !== SYMIDX_VERSION) {
return;
}
const indexData = json as PrebuiltIndex;
const idx = new SymbolIndex(indexData.records);
await idx.init();
if (indexData.ms) {
await idx.setMiniSearch(indexData.ms);
}
this.index = idx;
} catch (e) {
console.debug('Docs search index unavailable');
const topics = docsProvider() || [];
if (topics.length === 0) return;
const records: SymbolRecord[] = [];
for (const topic of topics) {
records.push(...extractDocRecords(topic));
}
if (records.length === 0) return;
const idx = new SymbolIndex(records);
await idx.init();
this.index = idx;
}
query(needle: string, limit: number): SearchHit[] {
async query(needle: string, limit: number): Promise<SearchHit[]> {
await this.ready();
if (!this.index) return [];
return this.index.query(needle, limit);
}
+6 -3
View File
@@ -67,9 +67,12 @@ export function openSearchHit(hit: SearchHit) {
if (wnd instanceof MemoryView) wnd.goToAddress(addr);
}
} else if (rec.source === 'docs') {
// External docs: open the URL in a new tab
const url = rec.detail || rec.file || '';
if (url) {
// In-IDE help pages route through ProjectWindows; anything else is an
// external URL opened in a new tab.
const url = rec.url || '';
if (url.startsWith('#help/')) {
projectWindows.createOrShow(url);
} else if (url) {
window.open(url, '_blank');
}
}
+5 -1
View File
@@ -73,7 +73,11 @@ export class SymbolIndex {
this.msOptions = {
idField: 'id',
fields: ['name', 'brief', 'detail'],
storeFields: ['id', 'name', 'kind', 'brief', 'detail', 'source', 'file', 'line'],
storeFields: ['id', 'name', 'kind', 'brief', 'detail', 'source', 'file', 'line', 'url'],
// Split on punctuation as well as whitespace so key names like
// "mod+Shift+Backspace" index as separate terms. Underscores are kept so
// identifiers (joy_read) stay one token.
tokenize: (text: string) => text.split(/[^A-Za-z0-9_]+/).filter((t: string) => t.length > 0),
};
}
+31 -88
View File
@@ -29,6 +29,7 @@ import { AssetEditorView } from "./views/asseteditor";
import { isMobileDevice } from "./views/baseviews";
import { AddressHeatMapView, BinaryFileView, BreakpointsView, MemoryMapView, MemoryView, ProbeLogView, ProbeSymbolView, RasterStackMapView, ScanlineIOView, VRAMMemoryView } from "./views/debugviews";
import { DisassemblerView, HeaderView, ListingView, PC_LINE_LOOKAHEAD, SourceEditor, setUppercaseOnly } from "./views/editors";
import { HELP_TOPICS, HelpView, helpTopicForView } from "./views/helpview";
import { CallStackView, DebugBrowserView } from "./views/treeviews";
import { ProjectWindows } from "./windows";
import { bpStore, resolveBreakpoints } from "./breakpoints";
@@ -448,6 +449,14 @@ function refreshWindowList() {
addWindowItem('#asseteditor', 'Asset Editor', () => {
return new AssetEditorView();
});
// Help: markdown docs shipped with the IDE. Only the index is listed in the
// sidebar; every topic still gets a create fn so #help/<id> deep links work.
//separate = true;
addWindowItem('#help', 'Help', () => new HelpView('#help'));
for (let topic of HELP_TOPICS) {
projectWindows.setCreateFunc('#help/' + topic.id, () => new HelpView(topic.id));
}
}
function highlightLines(path: string, hispec: string) {
@@ -1147,6 +1156,7 @@ function promptGoToAddress(): boolean {
function getGlobalShortcuts(): Shortcut[] {
var shortcuts: Shortcut[] = [];
if (platform && isPlatformReady()) {
shortcuts.push({ key: 'F1', label: 'Help', fn: showContextHelp });
// single-key pause/resume toggle (works while the editor is focused);
// mod+shift+h / mod+shift+g still work outside the editor.
// pushed first so its position doesn't shift when other chips come and
@@ -1778,6 +1788,8 @@ function setupDebugControls() {
uitoolbar.add('mod+shift+g', 'Resume', 'glyphicon-play', resume).prop('id', 'dbg_go');
// F8: single-key pause/resume toggle; chip shown via getGlobalShortcuts()
uitoolbar.add('F8', 'Pause/Resume', '', togglePauseResume);
// F1: context-sensitive help for the active view (editor/tool/platform)
uitoolbar.add('F1', 'Help', '', showContextHelp);
if (platform.restartAtPC) {
uitoolbar.add('mod+shift+a', 'Restart at Cursor', 'glyphicon-play-circle', restartAtCursor).prop('id', 'dbg_restartatline');
}
@@ -1851,8 +1863,6 @@ function setupDebugControls() {
$("#item_addfile_link").click(_addLinkFile);
$("#item_request_persist").click(() => requestPersistPermission(true, false));
$("#item_settings").click(openSettings);
$("#item_keyboard_shortcuts").click(openKeyboardShortcuts);
$("#item_asset_editor_help").click(openAssetEditorHelp);
updateDebugWindows();
// code analyzer?
if (platform.newCodeAnalyzer) {
@@ -1885,10 +1895,29 @@ function setupDebugControls() {
// opens an external page; don't let the placeholder href change our URL hash
$(a).click((e) => { e.preventDefault(); window.open(toolhelpurl!, '_8bws_help'); });
}
// internal IDE help (markdown docs shipped with the IDE)
{
let { li } = newDropdownListItem('#help', 'IDE Help');
$("#help_menu").prepend(li);
}
// all toolchain versions
$("#item_tool_versions").click(openToolVersions);
}
// F1: open the help topic that matches whatever is focused right now.
// Going through the hash means Back returns to the previous view.
function showContextHelp() {
if (!projectWindows) return;
var isEditor = projectWindows.getActive() instanceof SourceEditor;
var id = helpTopicForView(projectWindows.getActiveID(), isEditor);
var hash = '#help/' + id;
if (window.location.hash === hash) {
projectWindows.createOrShow(hash);
} else {
window.location.hash = hash;
}
}
function openToolVersions() {
const row = (name: string, kind: string, version: string) =>
`<tr><td>${name}</td><td>${kind}</td><td>${version}</td></tr>`;
@@ -1907,92 +1936,6 @@ function openToolVersions() {
});
}
function openKeyboardShortcuts() {
const isMac = /Mac|iPhone|iPad/.test(navigator.platform);
const mod = isMac ? '&#8984;' : 'Ctrl';
const shift = isMac ? '&#8679;' : 'Shift';
const shortcut = (keys: string, desc: string) =>
`<tr><td><kbd>${keys}</kbd></td><td>${desc}</td></tr>`;
bootbox.dialog({
title: "Keyboard shortcuts",
onEscape: true,
message: `
<table class="help">
<tr><th colspan="2">Editor</th></tr>
${shortcut(`${mod}+${shift}+F`, 'Search symbols, files, and docs')}
${shortcut('Tab', 'Insert to next tab stop, or indent selected range(s)')}
${shortcut(`${shift}+Tab`, 'Outdent line(s) or selected range(s)')}
${shortcut(`${mod}+${shift}+Backspace`, 'Delete line')}
<tr>
<td>Built-in</td>
<td>
Included CodeMirror shortcuts:<br>
<a target="_blank" href="https://codemirror.net/docs/ref/#commands.defaultKeymap">defaultKeymap</a>,
<a target="_blank" href="https://codemirror.net/docs/ref/#commands.standardKeymap">standardKeymap</a>,
<a target="_blank" href="https://codemirror.net/docs/ref/#commands.historyKeymap">historyKeymap</a>,
<a target="_blank" href="https://codemirror.net/docs/ref/#search.searchKeymap">searchKeymap</a>
</td>
</tr>
</table>`,
buttons: {
ok: { label: "OK", className: "btn-primary" }
}
});
}
function openAssetEditorHelp() {
const row = (field: string, dflt: string, desc: string) =>
`<tr><td><code>${field}</code></td><td>${dflt}</td><td>${desc}</td></tr>`;
bootbox.dialog({
title: "Asset Editor Reference",
onEscape: true,
message: `
<p>Add asset headers in source code as comments containing JSON format descriptors:</p>
<p>
C: <code>/*{w:8,h:8}*/</code> followed by data, terminated by <code>;</code><br>
ASM: <code>;;{w:8,h:8};;</code> followed by data, terminated by <code>;;</code>
</p>
<table class="help">
<tr><th colspan="3">Image Format</th></tr>
<tr><td><b>Field</b></td><td><b>Default</b></td><td><b>Description</b></td></tr>
${row('w', '<i>required</i>', 'Width in pixels')}
${row('h', '<i>required</i>', 'Height in pixels')}
${row('count', '1', 'Number of images')}
${row('bpp', '1', 'Bits per pixel')}
${row('np', '1', 'Number of bitplanes (total colors = 2<sup>bpp&times;np</sup>)')}
${row('bpw', '8', 'Bits per word (8, 16, 32)')}
${row('sl', 'ceil(w&times;bpp/bpw)', 'Words per scanline (stride)')}
${row('brev', 'false', 'Bit reverse: true = MSB is leftmost pixel')}
${row('flip', 'false', 'Flip vertically (y=0 is bottom row)')}
${row('skip', '0', 'Skip bytes at start of each image')}
${row('pofs', 'sl&times;h&times;count', 'Offset between bitplanes')}
${row('il', '0', 'Interleave images row by row (data stored as one wide block)')}
${row('remap', '&mdash;', 'Bit remapping table for address lines')}
${row('reindex', '&mdash;', 'Pixel-to-byte/bit remapping')}
${row('wpimg', 'sl&times;h', 'Words per image')}
${row('aspect', '1', 'Pixel aspect ratio for display')}
${row('xform', '&mdash;', 'CSS transform on canvas')}
${row('art', 'false', 'Artifact color mode: true = Apple II HGR (bit 7 toggles artifact color)')}
<tr><th colspan="3">Palette Format</th></tr>
<tr><td><b>Field</b></td><td><b>Default</b></td><td><b>Description</b></td></tr>
${row('pal', '&mdash;', 'Palette: number (e.g. 332 = 3R,3G,2B) or name (nes, vcs, c64, ap2lores, astrocade)')}
${row('n', '&mdash;', 'Number of palette entries')}
${row('layout', '&mdash;', 'Palette editor layout (nes, astrocade)')}
<tr><th colspan="3">Examples</th></tr>
<tr><td colspan="2"><code>/*{w:8,h:8,bpp:1,brev:1}*/</code></td><td>8x8 1bpp, MSB first (NES-style)</td></tr>
<tr><td colspan="2"><code>;;{w:8,h:5,count:4,il:1};;</code></td><td>4 interleaved 8x5 chars (stored as 32x5 block)</td></tr>
<tr><td colspan="2"><code>;;{w:7,h:8};;</code></td><td>7x8 1bpp, LSB first (Apple II HGR)</td></tr>
<tr><td colspan="2"><code>;;{w:8,h:8,art:true};;</code></td><td>8x8 with artifact color (Apple II HGR, bit 7 toggle)</td></tr>
<tr><td colspan="2"><code>/*{w:16,h:16,bpp:4,np:1}*/</code></td><td>16x16 4bpp</td></tr>
<tr><td colspan="2"><code>/*{pal:332,n:16}*/</code></td><td>16-entry RGB332 palette</pre></td></tr>
</table>`,
buttons: {
ok: { label: "OK", className: "btn-primary" }
},
size: "large"
});
}
function setupReplaySlider() {
var replayslider = $("#replayslider");
var clockslider = $("#clockslider");
+158
View File
@@ -0,0 +1,158 @@
import DOMPurify = require("dompurify");
import { ProjectView, newDiv } from "./baseviews";
import { setDocsProvider } from "../search/docssource";
// Help docs are Markdown files under src/docs. The esbuild plugin in
// build/md-loader.mjs renders each import to an HTML string at build time;
// src/md.d.ts supplies the ambient module type for `tsc`.
import indexMd from "../../docs/index.md";
import editorMd from "../../docs/editor.md";
import disasmMd from "../../docs/disasm.md";
import memoryMd from "../../docs/memory.md";
import memmapMd from "../../docs/memmap.md";
import vramMd from "../../docs/vram.md";
import memprobeMd from "../../docs/memprobe.md";
import crtprobeMd from "../../docs/crtprobe.md";
import probelogMd from "../../docs/probelog.md";
import scanlineioMd from "../../docs/scanlineio.md";
import symbolsMd from "../../docs/symbols.md";
import callstackMd from "../../docs/callstack.md";
import debugtreeMd from "../../docs/debugtree.md";
import breakpointsMd from "../../docs/breakpoints.md";
import asseteditorMd from "../../docs/asseteditor.md";
import managingFilesMd from "../../docs/managing-files.md";
import buildDirectivesMd from "../../docs/build-directives.md";
import assetHeadersMD from "../../docs/asset-headers.md";
import embeddingIdeMd from "../../docs/embedding-ide.md";
// Help is a normal ProjectView so it reuses ProjectWindows (tabs, hash
// routing, the window list). The registry and renderHelpBody() below are
// deliberately independent of the view so a split-pane help panel can reuse
// them without going through ProjectWindows.
export interface HelpTopic {
id: string; // stable id used in #help/<id>
title: string; // window/sidebar title
html: string; // rendered Markdown (still sanitized before display)
}
// Registry, in the order topics appear in the docs index.
export const HELP_TOPICS: HelpTopic[] = [
{ id: "index", title: "IDE Help", html: indexMd },
{ id: "editor", title: "Editor", html: editorMd },
{ id: "disasm", title: "Disassembly", html: disasmMd },
{ id: "memory", title: "Memory Browser", html: memoryMd },
{ id: "memmap", title: "Memory Map", html: memmapMd },
{ id: "vram", title: "VRAM Browser", html: vramMd },
{ id: "memprobe", title: "Memory Probe", html: memprobeMd },
{ id: "crtprobe", title: "CRT Probe", html: crtprobeMd },
{ id: "probelog", title: "Probe Log", html: probelogMd },
{ id: "scanlineio", title: "Scanline I/O", html: scanlineioMd },
{ id: "symbols", title: "Symbol Profiler", html: symbolsMd },
{ id: "callstack", title: "Call Stack", html: callstackMd },
{ id: "debugtree", title: "Debug Tree", html: debugtreeMd },
{ id: "breakpoints", title: "Breakpoints", html: breakpointsMd },
{ id: "asseteditor", title: "Asset Editor", html: asseteditorMd },
{ id: "managing-files", title: "Managing Files", html: managingFilesMd },
{ id: "build-directives", title: "Build Directives", html: buildDirectivesMd },
{ id: "asset-headers", title: "Asset Headers", html: assetHeadersMD },
{ id: "embedding-ide", title: "Embedding the IDE", html: embeddingIdeMd },
];
const HELP_BY_ID: { [id: string]: HelpTopic } = {};
for (let topic of HELP_TOPICS) HELP_BY_ID[topic.id] = topic;
// Make the bundled docs searchable from the IDE search palette.
setDocsProvider(() => HELP_TOPICS);
// Accepts "#help/editor", "help/editor", "#help", "editor.md", "editor" and
// returns a known topic id (falling back to the index).
export function resolveHelpId(id: string): string {
let s = id || "";
if (s.startsWith("#")) s = s.substring(1);
if (s.startsWith("help")) s = s.substring("help".length);
if (s.startsWith("/")) s = s.substring(1);
if (s.endsWith(".md")) s = s.substring(0, s.length - 3);
s = s.replace(/^\.\//, "");
return HELP_BY_ID[s] ? s : "index";
}
export function getHelpTopic(id: string): HelpTopic {
return HELP_BY_ID[resolveHelpId(id)];
}
// Map the active window id to its doc for F1 / future context-help pane.
// Tool windows have a dedicated doc; a focused source editor gets "editor".
const VIEW_HELP: { [viewId: string]: string } = {
"#disasm": "disasm",
"#memory": "memory",
"#memmap": "memmap",
"#vram": "vram",
"#memprobe": "memprobe",
"#crtprobe": "crtprobe",
"#probelog": "probelog",
"#scanlineio": "scanlineio",
"#symbols": "symbols",
"#callstack": "callstack",
"#debugtree": "debugtree",
"#breakpoints": "breakpoints",
"#asseteditor": "asseteditor",
};
export function helpTopicForView(viewId: string, isEditor: boolean): string {
if (isEditor) return "editor";
return (viewId && VIEW_HELP[viewId]) || "index";
}
// Turn relative Markdown links ("foo.md") into internal help routes
// ("#help/foo") so clicks stay in the IDE. Absolute links open in a new tab.
function rewriteHelpLinks(root: HTMLElement) {
const links = root.querySelectorAll("a[href]");
links.forEach((a) => {
const href = a.getAttribute("href");
if (!href) return;
if (/^[a-z][a-z0-9+.-]*:/i.test(href)) { // http:, https:, mailto:, ...
a.setAttribute("target", "_blank");
a.setAttribute("rel", "noopener noreferrer");
return;
}
if (href.startsWith("/") || href.startsWith("#")) return;
const hashIdx = href.indexOf("#");
const path = hashIdx >= 0 ? href.substring(0, hashIdx) : href;
if (!path.endsWith(".md")) return;
a.setAttribute("href", "#help/" + resolveHelpId(path));
a.removeAttribute("target");
});
}
// Sanitize and mount a topic. Everything goes through DOMPurify even though
// the docs ship with the app, so the render path stays safe if help ever
// comes from a GitHub project or other user-supplied content.
export function renderHelpBody(container: HTMLElement, id: string) {
const topic = getHelpTopic(id);
container.innerHTML = DOMPurify.sanitize(topic.html);
rewriteHelpLinks(container);
}
export class HelpView implements ProjectView {
maindiv: JQuery;
id: string;
constructor(id?: string) {
this.id = resolveHelpId(id || "index");
}
getPath(): string {
return "#help/" + this.id;
}
createDiv(parent: HTMLElement) {
this.maindiv = newDiv(parent, "vertical-scroll help-view");
renderHelpBody(this.maindiv[0], this.id);
return this.maindiv[0];
}
refresh(moveCursor: boolean) {
// static content; nothing to recompute on generic refreshes
}
}
+1
View File
@@ -110,6 +110,7 @@ export class ProjectWindows {
document.title = this.titlePrefix + mainName;
} else {
var viewName = id.startsWith('#headerview/') ? id.substring('#headerview/'.length)
: id.startsWith('#help/') ? id.substring('#help/'.length)
: id.startsWith('#') ? id : getFilenameForPath(id);
document.title = this.titlePrefix + mainName + ' | ' + viewName;
}
Vendored
+6
View File
@@ -0,0 +1,6 @@
// Ambient types for files transformed by build/md-loader.mjs.
// Keeps `tsc` happy when TypeScript files import Markdown.
declare module '*.md' {
const html: string;
export default html;
}
+100
View File
@@ -0,0 +1,100 @@
/**
* DocsSource tests - the IDE help docs are bundled as rendered HTML, so the
* docs index is built at runtime from help topics registered via
* setDocsProvider(). These tests exercise the pure HTML->records extraction
* and the source's query path without needing the `.md` imports (which only
* esbuild can resolve).
*/
import * as assert from 'assert';
import { DocsSource, setDocsProvider, extractDocRecords, htmlToText, DocTopic } from '../../src/ide/search/docssource';
const EDITOR_TOPIC: DocTopic = {
id: 'editor',
title: 'Editor',
html: [
'<h1>Editor</h1>',
'<p>The source editor is based on CodeMirror 6.</p>',
'<h2>Gutter</h2>',
'<p>Click the red circle to toggle a breakpoint.</p>',
'<h2>Keyboard shortcuts</h2>',
'<p>Press Tab to indent.</p>',
'<p>Press Backspace to outdent a selection.</p>',
].join(''),
};
describe('htmlToText', function () {
it('should strip tags, collapse whitespace, and decode entities', function () {
assert.strictEqual(htmlToText('<p>a &amp; b</p>\n <p>c</p>'), 'a & b c');
assert.strictEqual(htmlToText('<code>mod+shift+f</code>'), 'mod+shift+f');
});
});
describe('extractDocRecords', function () {
it('should emit a page record plus one record per heading', function () {
const records = extractDocRecords(EDITOR_TOPIC);
const names = records.map(r => r.name);
assert.ok(names.includes('Editor'), 'page title recorded');
assert.ok(names.includes('Gutter'));
assert.ok(names.includes('Keyboard shortcuts'));
assert.strictEqual(records.length, 3, 'page + two sections');
});
it('should route every record back to the help topic', function () {
for (const rec of extractDocRecords(EDITOR_TOPIC)) {
assert.strictEqual(rec.source, 'docs');
assert.strictEqual(rec.kind, 'doc');
assert.strictEqual(rec.url, '#help/editor');
assert.strictEqual(rec.file, 'Editor');
}
});
it('should capture section prose in brief/detail for search', function () {
const rec = extractDocRecords(EDITOR_TOPIC).find(r => r.name === 'Gutter');
assert.ok(rec);
assert.strictEqual(rec.brief, 'Click the red circle to toggle a breakpoint.');
});
it('should keep text past the first sentence in detail', function () {
const rec = extractDocRecords(EDITOR_TOPIC).find(r => r.name === 'Keyboard shortcuts');
assert.ok(rec);
assert.ok((rec.detail || '').includes('Backspace'), 'later paragraphs stay searchable');
});
it('should assign unique ids', function () {
const ids = extractDocRecords(EDITOR_TOPIC).map(r => r.id);
assert.strictEqual(new Set(ids).size, ids.length);
});
});
describe('DocsSource', function () {
after(function () {
setDocsProvider(() => []);
});
it('should find a doc by heading name', async function () {
setDocsProvider(() => [EDITOR_TOPIC]);
const ds = new DocsSource();
const hits = await ds.query('Gutter', 10);
assert.ok(hits.length >= 1, 'should find the Gutter section');
assert.ok(hits.some(h => h.record.name === 'Gutter'));
assert.strictEqual(hits[0].record.source, 'docs');
});
it('should find docs by prose in the body', async function () {
setDocsProvider(() => [EDITOR_TOPIC]);
const ds = new DocsSource();
const hits = await ds.query('Backspace', 10);
assert.ok(hits.length >= 1, 'prose search should match section text past the brief');
// Prose hits come back through MiniSearch; the url must survive so
// openSearchHit() can route to the help page instead of opening a tab.
assert.strictEqual(hits[0].record.url, '#help/editor');
});
it('should return nothing when no provider is registered', async function () {
setDocsProvider(() => []);
const ds = new DocsSource();
const hits = await ds.query('editor', 10);
assert.deepStrictEqual(hits, []);
});
});