mirror of
https://github.com/sehugg/8bitworkshop.git
synced 2026-10-25 00:02:16 +00:00
add ID docs to src/docs and search index, F1 for Help window
This commit is contained in:
1 parent
17c08bbc85
commit
3df69e7d04
37 files changed
+1956
-1613
No files matched your search
+78
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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.
|
||||
@@ -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">
|
||||
|
||||
Generated
+103
@@ -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
@@ -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",
|
||||
|
||||
@@ -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×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}*/` |
|
||||
@@ -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.
|
||||
|
||||
@@ -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`, … |
|
||||
| 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.
|
||||
@@ -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) |
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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`).
|
||||
|
||||
@@ -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>
|
||||
```
|
||||
@@ -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 » Revert to Original…**. To start from scratch, choose
|
||||
**New Project…** 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 » Show keyboard shortcuts / status bar**).
|
||||
|
||||
See [Editor](editor.md) for editor-specific shortcuts.
|
||||
@@ -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 » 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 » Delete Local Repository**.
|
||||
3. Type **YES** to confirm, then select **OK**.
|
||||
|
||||
### Publishing a project
|
||||
|
||||
1. From the menu, select **Sync » 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 » Push Changes to Repository…**, enter a
|
||||
commit message, then **Push Changes**.
|
||||
- **Pull:** select **Sync » Pull Latest from Repository**, then
|
||||
**OK**. All local files are overwritten; there is no merge in the
|
||||
browser.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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×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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
@@ -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
@@ -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(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/&#(?:0*39|x27);/gi, "'")
|
||||
.replace(/ /g, ' ')
|
||||
.replace(/…/g, '…')
|
||||
.replace(/—/g, '—')
|
||||
.replace(/–/g, '–')
|
||||
.replace(/’|‘/g, "'")
|
||||
.replace(/”|“/g, '"')
|
||||
.replace(/»/g, '»')
|
||||
.replace(/«/g, '«')
|
||||
.replace(/&/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);
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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 ? '⌘' : 'Ctrl';
|
||||
const shift = isMac ? '⇧' : '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×np</sup>)')}
|
||||
${row('bpw', '8', 'Bits per word (8, 16, 32)')}
|
||||
${row('sl', 'ceil(w×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×h×count', 'Offset between bitplanes')}
|
||||
${row('il', '0', 'Interleave images row by row (data stored as one wide block)')}
|
||||
${row('remap', '—', 'Bit remapping table for address lines')}
|
||||
${row('reindex', '—', 'Pixel-to-byte/bit remapping')}
|
||||
${row('wpimg', 'sl×h', 'Words per image')}
|
||||
${row('aspect', '1', 'Pixel aspect ratio for display')}
|
||||
${row('xform', '—', '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', '—', 'Palette: number (e.g. 332 = 3R,3G,2B) or name (nes, vcs, c64, ap2lores, astrocade)')}
|
||||
${row('n', '—', 'Number of palette entries')}
|
||||
${row('layout', '—', '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");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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 & 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, []);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user