Web64 Hardware Loader Runtime
Version: 2026-09-07
The hardware loader is an opt-in, synchronous native 6502 runtime for named-file reads from a 1541-family drive. It can receive raw PRGs, install raw payloads, and validate, decompress and transactionally install Web64 Packed data targets. It owns transport, validation and drive-code lifecycle. Your application owns memory, IRQs, music, display, file meanings, cache contents and transition policy.
Include <web64/loader.h> in C or web64/loader.inc in native assembly. Merely including declarations emits no loader. Calls exact-link their dependencies; raw reading does not link the packed decoder, and unpacking an existing memory container does not link disk transport. Pure assembly needs no C startup.
For a complete starting project, choose New > Template > Hardware Loader + Disk Mastering. The template provides native resident/raw/packed ASM targets, an editable disk set, memory reservations, IRQ-driven loading feedback and retry. Use Disk/Media > Build Dependencies > Run Disk; inspect per-target results under Code > Build Output. Its included DISK-GUIDE.md describes how to create more banks entirely in the IDE.
Hardware and scheduling contract
The supported protocol is the one-bit, long-filename Covert Bitops 1541 path on C64. Independent PAL and NTSC fixtures run against VICE true-drive 1541-II emulation, with repeated reads, errors, abort/retry and an actual SAVE/read-back. This is not a claim of physical-hardware testing. Other IEC devices, SD2IEC, 1581, CMD drives, cartridges and accelerated CPUs are not supported by this contract. Unsupported detected drive types return an error.
There is no automatic KERNAL fallback. Existing <web64/disk.h> operations remain available as an explicitly chosen, separate compatible path; they do not inherit the fast loader's presentation or transactional guarantees.
All loading calls block the mainline until success or failure. There is no start/poll/finish API and no hidden background task. A caller-owned IRQ can continue music and lightweight presentation during fast reads. That does not make a blocked mainline menu responsive: do not start title-screen prefetch if its input or display updates depend on that mainline returning every frame.
Initialization and reinstallation use normal KERNAL IEC communication and may stall presentation. Ordinary SAVE is also non-fluid. Silence or deliberately hold the music and provide a static busy state for those phases if necessary. The fast-read fixtures preserve a one-frame presentation cadence; this is a measured fixture result, not a promise for arbitrary heavy IRQ handlers.
Create the complete disk inside Web64
- Create a resident C, ASM or hybrid boot target in Build Targets. Reserve resident code, C stack if used, music, display and loader workspaces.
- Author data with native asset editors and ordinary virtual source files. Assemble each bank at its intended installed origin. SID Tracker output should be included using its generated C64 payload offset/size, not the PSID container header. See the IDE manual's SID payload-bank instructions.
- Keep the bank a normal PRG target for raw loading, or choose Packed data (Exomizer). Choose its staging address, maximum packed size and, optionally, fixed-size independent blocks. This uses the browser-native encoder, not an external Exomizer executable. The native editor source remains authoritative.
- In Disk/Media, add target outputs as logical PRG files. Set banks to Raw data, the boot program to Runnable (SYS), and place them on a D64. Packed targets reject SYS wrapping. Give each bank the exact DOS name used by the application; filenames are 1–16 nonzero PETSCII bytes.
- Use Build Dependencies, then Run Disk. Native source/asset edits and included dependencies invalidate packed targets too. Rebuild and remount after edits; the already mounted image cannot change itself.
No generated source needs to be hand-edited, no project JSON tailoring is required, and no prepacked source artifact or host filesystem tool is involved. Disk placement controls are optional. Directory order does not schedule loads.
API and results
Every status-returning function returns uint8_t: zero means success. Except for a rejected busy/reentrant call, web64_loader_status() reports the last operation's result. A busy call returns BUSY without mutating the active call.
| Call | Purpose |
|---|---|
web64_loader_init(device, sector, ticks) | Detect a supported drive, upload its program and configure caller storage. Call again after a drive power-cycle or uncertain configuration. |
web64_loader_read(name, length, stage, capacity) | Read a PRG's body into bounded staging, consuming its two-byte load address separately. |
web64_loader_last_size() / web64_loader_last_origin() | Received body byte count and original PRG load address; use after a successful read. |
web64_loader_load_raw(&request) | Stage a complete PRG, check origin and size, then copy to the allowed destination. |
web64_loader_load_packed(&request) | Stage a complete W64X PRG and perform the checked packed transaction. |
web64_loader_unpack(&request) | Validate/install an already staged W64X body; does not require a drive. |
web64_loader_abort() | IRQ-safe cancellation request during a fallible read/decode phase. Idle/commit requests are ignored. |
web64_loader_shutdown() | Detach drive code and invalidate fast reads before normal KERNAL I/O. |
web64_loader_reinitialize_after_save() | Reinstall using the last valid device/sector/clock configuration. |
All status names have the prefix WEB64_LOADER_ in C and ASM:
| Value | Name | Meaning / response |
|---|---|---|
| 0 | OK | Requested data installed or operation completed. |
| 1 | NOT_FOUND | Named file absent; check mounted media and exact DOS name. |
| 2 | TRANSPORT | Serial/read failure or timeout; initialize before retrying. |
| 3 | UNAVAILABLE | Device/media unavailable; restore it and initialize. |
| 4 | HEADER | Bad W64X magic/version or incompatible metadata/trailer. |
| 5 | INTEGRITY | Container or decoded CRC mismatch; reject the bank. |
| 6 | PACKED_SIZE | Staging too small, truncated body or inconsistent packed layout. |
| 7 | UNPACKED_SIZE | Invalid/overflowing decoded record size. |
| 8 | RANGE | Invalid, overflowing or forbidden address span. |
| 9 | WORKSPACE | Overlapping storage or insufficient decode workspace. |
| 10 | DECOMPRESSION | Malformed/unsupported bounded stream or wrong decoded extent. |
| 11 | ABORTED | Accepted cancellation; initialize/reinstall before another read. |
| 12 | NOT_INITIALIZED | Drive code is not valid, or no saved configuration exists. |
| 13 | INVALID_ARGUMENT | Bad filename, device, sector, clock, banking, interrupt state or block selector. |
| 14 | UNSUPPORTED_DRIVE | Detected drive is outside the implemented protocol. |
| 15 | BUSY | A mainline loader operation is already active. |
Check success before using getters, entering an overlay, starting a new SID player or displaying a new bank. Do not interpret a nonzero status as EOF.
Staging, packed transactions and caches
web64_loader_read receives into the exact supplied staging address regardless of the PRG's origin. Its size excludes the two-byte PRG prefix. Staging can be partially modified on a failed receive. It must never alias live data.
For whole-file raw installation, the PRG origin must equal destination and the received body must fit capacity. Workspace fields are unused. Destination is unchanged until the complete receive succeeds. For whole-file packed loading, the PRG prefix must equal stage, matching the Packed data target's staging setting. Embedded W64X destinations must lie inside the allowed destination span. They are restore-at-origin records, not implicit relocation requests.
The packed transaction checks format, complete directory, lengths, stream trailers, address bounds and the outer CRC. Selected blocks decode into separate workspace, with input/output/history bounds and decoded CRC checks. All selected blocks pass before the first live destination write. Even malformed compressed bytes with a recomputed outer CRC cannot partially replace a bank. Integrity checks detect corruption; CRC is not cryptographic authentication.
Choose a zero-based block, or WEB64_LOADER_ALL_BLOCKS (255). All directory records are validated; only selected streams are decoded and installed. ALL requires scratch equal to the sum of selected decoded lengths. An independent block target has multiple records sharing one installed address: normally select one. ALL installs them in directory order, so the last record wins at that address.
Retain an immutable container in caller-owned cache RAM and call unpack with the selected record whenever needed. There is no runtime cache policy or hidden allocation. Do not overwrite staging, descriptors or workspace from an IRQ during a call. The final commit closes the cancellation window atomically, then copies with the caller's interrupt state restored. Multi-block success is not an atomic display-page flip: readers of a replaced bank must remain paused until the call returns. The IRQ may use other resident banks throughout.
C request layouts
The SDK supplies packed, exact-width struct declarations under both Web64-C ABIs. Pointers and uint16_t fields are little-endian two-byte words; byte fields have no padding. Use these types instead of guessing offsets.
| Type | Fields in byte order | Size |
|---|---|---|
web64_loader_request | name pointer 0, name_length byte 2, stage pointer 3, stage_capacity word 5, destination pointer 7, capacity word 9, workspace pointer 11, workspace_capacity word 13, block byte 15 | 16 |
web64_loader_packed_request | stage pointer 0, length word 2, destination pointer 4, capacity word 6, workspace pointer 8, workspace_capacity word 10, block byte 12 | 13 |
For example, after the application has installed its IRQ and reserved these disjoint addresses (they are choices for this snippet, not runtime reservations):
#include <web64/loader.h>
extern volatile uint16_t disk_ticks; /* incremented by the application's IRQ */
web64_loader_request bank;
uint8_t load_bank(void) {
uint8_t status;
status = web64_loader_init(8, (uint8_t*)0xbf00, &disk_ticks);
if (status != WEB64_LOADER_OK) return status;
bank.name = "DATA";
bank.name_length = 4;
bank.stage = (uint8_t*)0x6000;
bank.stage_capacity = 2048;
bank.destination = (uint8_t*)0x9000;
bank.capacity = 2048;
bank.workspace = (uint8_t*)0x7000;
bank.workspace_capacity = 2048;
bank.block = WEB64_LOADER_ALL_BLOCKS;
return web64_loader_load_packed(&bank);
}
Initialize once in normal use, not before every successful read. Keep the previous bank active on error and offer a retry when media is available.
Direct assembly ABI
The native implementation is authoritative. C _web64_rt calls and assembly macros write the same exact-width named windows before JSR:
| Entry | Argument window bytes, in order |
|---|---|
_web64_loader_init | device 1, sector 2, ticks 2 |
_web64_loader_read | name 2, length 1, stage 2, capacity 2 |
_web64_loader_load_raw, _web64_loader_load_packed, _web64_loader_unpack | request 2 |
| Other public entries | No argument window |
Labels are _web64_loader_read__arg_stage, for example; window size, field width and offset symbols are generated by the same ABI registry. web64_rt_prepare_* macros set windows; web64_rt_call_* also call the routine. Macros take immediate values/addresses. For changing runtime values, write the named window explicitly. WEB64_LOADER_REQUEST_* and WEB64_LOADER_PACKED_REQUEST_* provide descriptor offsets, including STAGE_CAPACITY, WORKSPACE_CAPACITY and SIZE as applicable.
.include "web64/loader.inc"
; Application has configured its clock/IRQ, $01=$35 and enabled interrupts.
web64_rt_call_loader_init 8, $bf00, disk_ticks
cmp #WEB64_LOADER_OK
bne failed
web64_rt_call_loader_load_packed bank_request
cmp #WEB64_LOADER_OK
bne failed
; The complete selected data is now safe to use.
rts
failed:
rts
bank_name: .text "DATA"
bank_request:
.word bank_name
.byte 4
.word $6000,2048,$9000,2048,$7000,2048
.byte WEB64_LOADER_ALL_BLOCKS
Status returns in A. Sixteen-bit getters return low byte in A and high byte in X. A/X/Y and arithmetic flags are caller-clobbered; compare A explicitly, not carry. The entry may clear decimal mode. Normal calls balance the hardware stack. abort clobbers A/flags, makes no IEC calls and is the only interrupt-callable mutation. The other operations are single-instance, mainline-only, non-reentrant.
RAM, IRQ, SID and machine ownership
- Transport requires a caller-owned, page-aligned 256-byte sector buffer in RAM, outside I/O, code, stack, staging and live banks. Page
$ff00is rejected. Device numbers are 8–30. The monotonic clock is a caller-owned 16-bit word in persistent RAM, incremented by IRQ. Do not reset it during a read. The high byte supplies a bounded watchdog (roughly 10–15 PAL seconds for a dead peer). - Read/whole-file operations require
$01low bits5(normally$35) and interrupts enabled. They do not install vectors or change the VIC mode. Buffers cannot wrap past$ffff, enter zero page/stack, overlap each other, or include$d000–$dfffI/O. An exact one-past endpoint of$10000is representable through start/size. - Unpack-only uses the caller's memory mapping and needs no clock. Its staging, allowed destination and scratch spans must be disjoint and genuinely owned RAM. Hardware I/O is not a transactional destination. The compiler registers known runtime placements and ZP ownership; the caller must also reserve all dynamic spans against resident code, SID data, sprites and C stack.
- The decoder owns
$60–$68during unpack. Transport owns$74–$79persistently from initialization until shutdown. Packed/read/install calls use$fb–$feas call scratch, compatible with Web64-C's documented call-scratch overlay. An IRQ using any active bytes must save and restore them. These are not the compiler's$02–$18stack-ABI pseudo-registers. - Transport manipulates CIA2 IEC lines in
$dd00, preserving its VIC-bank low bits. Give it exclusive IEC access and retain the normal IEC direction setup. Do not perform other disk/bus operations concurrently or rewrite CIA2 from the presenter. The fast read loop does not take over a CIA timer, VIC raster IRQ, SID player, sprites or the application frame counter. - Initialization/reinstallation temporarily maps KERNAL ROM using
$36, restores the previous processor-port value and console-message mode, and uses ordinary KERNAL workspace. If IRQs are active, supply both the RAM$fffeentry and a KERNAL-compatible$0314entry. Account for NMI/RESTORE: install a valid RAM$fffahandler and decide whether to disable CIA2 NMI sources. The runtime does not take ownership of RESTORE, vectors, IRQ acknowledgement orRTI. - Keep music code/data outside every staging, scratch and install span used while that player runs. An IRQ must preserve A/X/Y and its scratch, finish promptly, and never call the loader recursively or invoke KERNAL I/O. No arbitrary C callback executes inside the serial protocol.
- The loader has no hidden C software stack. Native unpack measured 12 bytes of extra hardware stack; the PAL integration fixture's canary footprint was 18 bytes including its small IRQ, KERNAL SAVE and recovery. These are observations, not worst-case bounds for user code. Reserve at least 128 free hardware-stack bytes for this workflow and verify your own nested IRQ/KERNAL usage. A C stack placed under ROM must remain RAM-visible whenever C code accesses it.
Optional caller-placed runtime images
Normal calls let the exact linker place code beside the application. For a tightly banked ASM or hybrid program, read-only SDK includes can emit boot-copy images of the same implementation, without a second private port:
web64/loader-transport-runtime.inc, loader-state-runtime.inc, loader-packed-runtime.inc, loader-decoder-runtime.inc, loader-raw-runtime.inc (all under web64/).
For each selected image define WEB64_LOADER_<NAME>_ADDRESS, include it once, and copy web64_loader_<name>_image_size bytes from web64_loader_<name>_image to that address before calling it. The compiler checks logical execution spans against other placed images, assembled data and the configured C stack. Transport must be page-aligned in RAM below $d000 because it executes while KERNAL is visible. Other kernels require writable RAM; they use self-modifying operands. Keep their boot image alive until copied, and never overwrite their logical code/state with a loaded bank. Advanced includes intentionally emit whole module images; the normal linker can trim unused entries more tightly. Ordinary loader.inc still emits no data.
SAVE and error recovery
Use this explicit sequence; no reliable automatic detection of arbitrary external drive writes exists:
- Finish the current loader call. Call
web64_loader_shutdown()and check OK. - Map KERNAL and perform the application's normal SAVE. Preserve its result separately; loading status and save status are different operations.
- Call
web64_loader_reinitialize_after_save()before the next fast read. - Restore the application's normal mapping, IRQ and presentation policy. If reinstall failed, leave fast loading unavailable and offer a retry using
initonce the drive/media is restored.
Reads after shutdown are rejected with NOT_INITIALIZED. Do not call ordinary SAVE while pretending the fast-loader state is still valid. A drive power-cycle, hard error or accepted abort may also require fresh initialization. The runtime never silently retries forever; the application decides when another attempt is appropriate. Do not execute a bank merely because a previous load succeeded.
Format and measured costs
W64X v1's single shared IDE/runtime contract is backward Exomizer P39/M255, 8-byte header, 10-byte records and CRC16/XMODEM. See the IDE manual for byte offsets. Only that bounded profile is supported. This runtime does not fix or expand the separate unrestricted c64lib Exomizer decoder.
Code size depends on called entries, origin and alignment. The source includes independent native closure and CPU-cycle fixtures, plus real PAL/NTSC true-drive fixtures; decoded bytes are compared byte-for-byte, not accepted by appearance. Transport body is about 1.6 KiB, packed validation/CRC about 2.4 KiB, and bounded decoder about 0.7 KiB. Whole-file wrappers add their checked staging policy. Raw-only code does not pay the decoder cost. Sector RAM is 256 bytes; staging is caller-sized; transactional scratch is the sum of selected decoded lengths.
The altered upstream implementations and redistribution conditions are recorded in Hardware loader notices. Retain those notices when distributing substantial loader source or binaries.
Troubleshooting
- Immediate INVALID_ARGUMENT: check
$01, enabled IRQs, device range, filename length, sector alignment and a valid persistent clock pointer. - UNAVAILABLE or timeout: mount a valid D64 in the selected true-drive unit; do not substitute browser-side file injection for IEC testing. Restore the actual drive/image and initialize before retrying.
- PACKED_SIZE / HEADER: use the Packed data target's exact Raw data artifact, correct staging address, and enough staging capacity. No SYS or PSID header belongs inside a W64X payload.
- WORKSPACE / RANGE: compare full declared spans, not only bytes used by the current record. Staging, scratch, destination, code, sector and stacks cannot borrow each other's live RAM.
- Music glitches: distinguish a fast read from ordinary SAVE/reinstallation; audit IRQ scratch preservation, worst-case cycles and SID bank ownership.
- Stale data after editing: Build Dependencies, rebuild the disk, remount it and restart/retry. Editing a native asset does not rewrite a mounted drive.
- First read after SAVE fails: shutdown/invalidate before SAVE and explicitly reinstall afterward. Never assume the drive still contains its uploaded code.