gigatron/rom/Contrib/lb3361/runjs/html/loader.js
2025-01-28 19:17:01 +03:00

335 lines
9.6 KiB
JavaScript

import {
HSYNC,
VSYNC,
} from './vga.js';
import {
BUTTON_DOWN,
BUTTON_A,
} from './gamepad.js';
const {
Observable,
Subject,
concat,
defer,
range,
} = rxjs;
const {
concatMap,
concatAll,
finalize,
} = rxjs.operators;
const MAX_PAYLOAD_SIZE = 60;
const START_OF_FRAME = 'L'.charCodeAt(0);
const INIT_CHECKSUM = 'g'.charCodeAt(0);
/** Repeat an Observable count times
* @param {number} count
* @param {Observable} observable
* @return {Observable}
*/
function replicate(count, observable) {
// return concat(...new Array(count).fill(observable));
let go = (observer) => {
if (count-- > 0) {
observable.subscribe({
next: (value) => observer.next(value),
error: (err) => observer.error(err),
complete: () => go(observer),
});
} else {
observer.complete();
}
};
return Observable.create(go);
}
/** Loader */
export class Loader {
/** Create a new Loader
* @param {Gigatron} cpu
*/
constructor(cpu) {
this.cpu = cpu;
this.strobes = new Subject();
}
/** load a gt1 file
* @param {File} file
* @return {Observable}
*/
load(file) {
return this.readFile(file).pipe(
concatMap((buffer) => {
let data = new DataView(buffer);
return concat(
this.startLoader(),
defer(() => {
// Send one frame with false checksum to force
// a checksum resync at the receiver
this.checksum = 0;
return this.sendFrame(0xff, 0);
}),
defer(() => {
// Setup checksum properly
this.checksum = INIT_CHECKSUM;
return this.sendSegments(data);
}));
}),
finalize(() => {
// Set the input register back to quiesced state
this.cpu.inReg = 0xff;
}));
}
/** read a file returning a Promise
* @param {File} file
* @return {Observable}
*/
readFile(file) {
return Observable.create((observer) => {
let reader = new FileReader();
reader.onload = (event) => {
observer.next(reader.result);
observer.complete();
};
reader.onerror = (event) => {
observer.error(new Error('FileReader error'));
};
reader.readAsArrayBuffer(file);
});
}
/** start the loader on the gigatron
* @return {Observable}
*/
startLoader() {
return concat(
defer(() => {
this.cpu.reset();
return replicate(100, this.atPosedge(VSYNC));
}),
replicate(10, this.pressButton(BUTTON_DOWN, 1, 1)),
this.pressButton(BUTTON_A, 1, 60)
);
}
/** simulate a button press
* @param {number} bit
* @param {number} downTime
* @param {number} upTime
* @return {Observable}
*/
pressButton(bit, downTime, upTime) {
return concat(
defer(() => {
this.cpu.inReg = bit ^ 0xff;
return replicate(downTime, this.atPosedge(VSYNC));
}),
defer(() => {
this.cpu.inReg = 0xff;
return replicate(upTime, this.atPosedge(VSYNC));
})
);
}
/** load sections from data until busy
* @param {DataView} data
* @return {Observable<Observable<T>>}
*/
sendSegments(data) {
return Observable.create((observer) => {
let offset = 0;
while (offset < data.byteLength) {
if (data.getUint8(offset) == 0 && offset != 0) {
// start address segment
offset += 1;
let startAddr = data.getUint16(offset);
offset += 2;
if (startAddr != 0) {
observer.next(this.sendStartCommand(startAddr));
}
break;
} else {
// data segment
let addr = data.getUint16(offset);
offset += 2;
let size = data.getUint8(offset);
offset += 1;
if (size == 0) {
size = 256;
}
let payload = new DataView(
data.buffer,
data.byteOffset + offset,
size);
observer.next(this.sendDataSegment(addr, payload));
offset += size;
}
}
if (offset > data.byteLength) {
observer.error(new Error('Last segment exceeds file size'));
}
observer.complete();
}).pipe(concatAll());
}
/** send a start command
* @param {number} addr
* @return {Observable}
*/
sendStartCommand(addr) {
return this.sendFrame(START_OF_FRAME, addr);
}
/** send a data block
* @param {number} addr
* @param {DataView} data
* @return {Observable}
*/
sendDataSegment(addr, data) {
return Observable.create((observer) => {
let buffer = data.buffer;
let size = data.byteLength;
let offset = data.byteOffset;
let bytesInPage = 256 - (addr & 255);
if (size > bytesInPage) {
observer.error(new Error('Segment crosses page boundary'));
} else {
while (size != 0) {
let n = Math.min(size, MAX_PAYLOAD_SIZE);
let payload = new DataView(buffer, offset, n);
observer.next(this.sendFrame(
START_OF_FRAME, addr, payload));
addr += n;
offset += n;
size -= n;
}
observer.complete();
}
}).pipe(concatAll());
}
/** send the payload frame
* @param {number} firstByte
* @param {number} addr
* @param {DataView} payload
* @return {Observable}
*/
sendFrame(firstByte, addr, payload) {
return concat(
this.atNegedge(VSYNC),
// account for 2 cycles delay in 74HCT595 and ?
this.atPosedge(HSYNC),
this.atPosedge(HSYNC),
this.sendDataBits(firstByte, 8),
defer(() => {
this.checksum = (this.checksum + (firstByte << 6)) & 0xff;
return this.sendDataBits(payload ? payload.byteLength : 0, 6);
}),
this.sendDataBits(addr & 0xff, 8),
this.sendDataBits(addr >> 8, 8),
this.sendDataBytes(payload),
defer(() => {
this.checksum = (-this.checksum) & 0xff;
return this.sendBits(this.checksum, 8);
}));
}
/** send bytes from payload
* @param {Uint8Array} payload
* @return {Observable}
*/
sendDataBytes(payload) {
return range(0, MAX_PAYLOAD_SIZE).pipe(
concatMap((offset) => {
let byte = (payload && offset < payload.byteLength) ?
payload.getUint8(offset) : 0;
return this.sendDataBits(byte, 8);
}));
}
/** send bits and add to checksum
* @param {number} value - byte containing bits to send (msb first)
* @param {number} n - number of bits to send
* @return {Observable}
*/
sendDataBits(value, n) {
return defer(() => {
this.checksum = (this.checksum + value) & 0xff;
return this.sendBits(value, n);
});
}
/** shift one bit into inReg
* @param {number} bit
*/
shiftBit(bit) {
this.cpu.inReg = ((this.cpu.inReg << 1) & 0xff) | (bit ? 1 : 0);
}
/** send bits
* @param {number} value - byte containing bits to send (msb first)
* @param {number} n - number of bits to send
* @return {Observable}
*/
sendBits(value, n) {
return range(0, n).pipe(
concatMap((i) => {
this.shiftBit(value & (1 << (n - i - 1)));
return this.atPosedge(HSYNC);
}));
}
/** wait for negedge of signal
* @param {number} mask
* @return {Observable}
*/
atNegedge(mask) {
return Observable.create((observer) => {
let prev = this.cpu.out;
let subscription = this.strobes.subscribe((curr) => {
if (prev & ~curr & mask) {
observer.complete();
subscription.unsubscribe();
} else {
prev = curr;
}
});
});
}
/** wait for posedge of signal
* @param {number} mask
* @return {Observable}
*/
atPosedge(mask) {
return Observable.create((observer) => {
let prev = this.cpu.out;
let subscription = this.strobes.subscribe((curr) => {
if (~prev & curr & mask) {
observer.complete();
subscription.unsubscribe();
} else {
prev = curr;
}
});
});
}
/** advance one tick */
tick() {
if ((this.out ^ this.cpu.out) & (HSYNC | VSYNC)) {
this.out = this.cpu.out;
this.strobes.next(this.out);
}
}
}