Compare commits

...

2 commits

Author SHA1 Message Date
Alexander Bass b550a62af8 bulk commit before breaking everything 2024-03-25 23:03:20 -04:00
Alexander Bass 96f1104734 clean up remaining non conforming file and method names 2024-03-13 02:25:15 -04:00
22 changed files with 227 additions and 75 deletions

View file

@ -11,7 +11,7 @@ export type TempInstrState = {
params: Array<u8>;
};
function init_banks(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
function initBanks(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
const banks = [];
for (let i = 0; i < 4; i++) {
banks.push(new Uint8Array(256));
@ -19,8 +19,8 @@ function init_banks(): [Uint8Array, Uint8Array, Uint8Array, Uint8Array] {
return banks as [Uint8Array, Uint8Array, Uint8Array, Uint8Array];
}
export class Computer {
private banks: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] = init_banks();
export default class Computer {
private banks: [Uint8Array, Uint8Array, Uint8Array, Uint8Array] = initBanks();
private registers: Uint8Array = new Uint8Array(8);
private call_stack: Array<u8> = [];
private carry_flag: boolean = false;
@ -41,7 +41,7 @@ export class Computer {
code: current_byte,
});
console.log(`Invalid instruction: ${formatHex(current_byte)}`);
this.step_forward();
this.stepForward();
this.events.dispatch(CpuEvent.Cycle);
return;
}
@ -59,7 +59,7 @@ export class Computer {
}
if (this.current_instr.pos === this.program_counter && this.current_instr.params.length > 0) {
this.step_forward();
this.stepForward();
this.events.dispatch(CpuEvent.Cycle);
return;
}
@ -75,7 +75,7 @@ export class Computer {
this.current_instr.params[this.current_instr.params_found] = current_byte;
this.current_instr.params_found += 1;
if (this.current_instr.params.length !== this.current_instr.params_found) {
this.step_forward();
this.stepForward();
this.events.dispatch(CpuEvent.Cycle);
return;
}
@ -92,7 +92,7 @@ export class Computer {
this.current_instr = null;
if (execution_post_action_state.should_step) {
this.step_forward();
this.stepForward();
}
this.events.dispatch(CpuEvent.Cycle);
}
@ -163,17 +163,6 @@ export class Computer {
this.events.dispatch(CpuEvent.SetVramBank, { bank });
}
reset(): void {
this.events.dispatch(CpuEvent.Reset);
this.banks = init_banks();
this.registers = new Uint8Array(8);
this.call_stack = [];
this.current_instr = null;
this.program_counter = 0;
this.carry_flag = false;
this.vram_bank = 3;
}
initEvents(ui: UiCpuSignalHandler): void {
ui.listen(UiCpuSignal.RequestCpuCycle, (cycle_count) => {
for (let i = 0; i < cycle_count; i++) this.cycle();
@ -185,6 +174,31 @@ export class Computer {
ui.listen(UiCpuSignal.RequestProgramCounterChange, ({ address }) => {
this.setProgramCounter(address);
});
ui.listen(UiCpuSignal.RequestCpuSoftReset, () => this.softReset());
}
softReset(): void {
this.events.dispatch(CpuEvent.SoftReset);
for (let i = 0; i < 8; i++) this.setRegister(i as u3, 0);
while (this.popCallStack() !== null) 0;
this.setVramBank(DEFAULT_VRAM_BANK);
this.setCarry(false);
this.current_instr = null;
this.setProgramCounter(0);
this.setBank(0);
}
reset(): void {
this.events.dispatch(CpuEvent.Reset);
// Hard reset
this.banks = initBanks();
// Soft reset
for (let i = 0; i < 8; i++) this.setRegister(i as u3, 0);
while (this.popCallStack() !== null) 0;
this.setVramBank(DEFAULT_VRAM_BANK);
this.setCarry(false);
this.current_instr = null;
this.setProgramCounter(0);
this.setBank(0);
}
loadMemory(program: Array<u8>): void {
@ -205,7 +219,7 @@ export class Computer {
return this.banks;
}
private step_forward(): void {
private stepForward(): void {
this.program_counter = m256(this.program_counter + 1);
this.events.dispatch(CpuEvent.ProgramCounterChanged, { counter: this.program_counter });
}

View file

@ -21,6 +21,7 @@ export enum CpuEvent {
Cycle,
Print,
Reset,
SoftReset,
Halt,
MemoryAccessed,
SwitchBank,
@ -28,7 +29,7 @@ export enum CpuEvent {
SetVramBank,
}
type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.Cycle;
type VoidDataCpuEventList = CpuEvent.Halt | CpuEvent.Reset | CpuEvent.SoftReset | CpuEvent.Cycle;
interface CpuEventMap {
[CpuEvent.MemoryChanged]: { address: u8; bank: u2; value: u8 };
@ -67,11 +68,12 @@ export enum UiCpuSignal {
RequestMemoryChange,
RequestRegisterChange,
RequestCpuReset,
RequestCpuSoftReset,
RequestMemoryDump,
RequestProgramCounterChange,
}
type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset;
type VoidDataUiCpuSignalList = UiCpuSignal.RequestCpuReset | UiCpuSignal.RequestCpuSoftReset;
interface UiCpuSignalMap {
[UiCpuSignal.RequestCpuCycle]: number;

View file

@ -3,11 +3,11 @@
* @copyright Alexander Bass 2024
* @license GPL-3.0
*/
import { Computer } from "./computer";
import Computer from "./computer";
import UI from "./ui";
import { $ } from "./etc";
import { ISA } from "./instructionSet";
import { generateIsa } from "./isaGenerator";
import { UI } from "./ui";
import { u8 } from "./num";
import "./style/style.scss";
@ -47,6 +47,8 @@ function main(): void {
window.comp = computer;
window.ui = ui;
// Todo, move to ui component
// or move to documentation
$("ISA").textContent = generateIsa(ISA);
let fire = false;

View file

@ -0,0 +1,5 @@
.hover_text_box {
border: 5px solid yellow;
background-color: black;
user-select: none;
}

View file

@ -1,4 +1,5 @@
@use "memory_registers";
@use "hover_text_box";
@use "windows";
@use "buttons";
@use "vars";

View file

@ -5,18 +5,18 @@ import UiComponent, { UiComponentConstructor } from "./ui/uiComponent";
import MemoryView from "./ui/components/memoryView";
import frequencyIndicator from "./ui/components/frequencyIndicator";
import RegisterView from "./ui/components/registerView";
import BankSelector from "./ui/components/bank_view_selector";
import BankSelector from "./ui/components/bankViewSelector";
import EditButton from "./ui/components/editButton";
import pausePlay from "./ui/components/pausePlay";
import SaveLoad from "./ui/components/saveLoad";
import ResetButtons from "./ui/components/reset_buttons";
import ResetButtons from "./ui/components/resetButtons";
// Window Components
import InstructionExplainer from "./ui/windows/instructionExplainer";
import Screen from "./ui/windows/screen";
import Printout from "./ui/windows/printout";
import BankVisualizer from "./ui/windows/bank_visualizer";
import BankVisualizer from "./ui/windows/bankVisualizer";
export class UI {
export default class UI {
ui_events: UiEventHandler = new UiEventHandler();
cpu_signaler: UiCpuSignalHandler = new UiCpuSignalHandler();
private components: Array<UiComponent> = [];
@ -47,9 +47,8 @@ export class UI {
}
initEvents(cpu_events: CpuEventHandler): void {
cpu_events.listen(CpuEvent.Reset, () => {
this.reset();
});
cpu_events.listen(CpuEvent.Reset, () => this.reset());
cpu_events.listen(CpuEvent.SoftReset, () => this.softReset());
for (const c of this.components) if (c.initCpuEvents) c.initCpuEvents(cpu_events);
}
@ -57,4 +56,8 @@ export class UI {
reset(): void {
for (const c of this.components) if (c.reset) c.reset();
}
softReset(): void {
for (const c of this.components) if (c.softReset) c.softReset();
}
}

View file

@ -6,7 +6,7 @@ import UiComponent from "../uiComponent";
export default class BankSelector implements UiComponent {
container: HTMLElement;
events: UiEventHandler;
private bank_buttons: Array<HTMLButtonElement>;
bank_buttons: Array<HTMLButtonElement>;
constructor(element: HTMLElement, events: UiEventHandler) {
this.container = element;
this.events = events;
@ -28,8 +28,13 @@ export default class BankSelector implements UiComponent {
this.container.appendChild(bank_boxes);
}
reset(): void {
for (const b of this.bank_buttons) b.classList.remove("selected");
this.bank_buttons[0].classList.add("selected");
}
softReset(): void {
this.reset();
}
}

View file

@ -12,18 +12,18 @@ export default class EditButton implements UiComponent {
this.cpu_signals = cpu_signals;
const image = el("img").at("src", "pencil.png").st("width", "20px").st("height", "20px").fin();
this.container.classList.add("editor_toggle");
this.container.addEventListener("click", () => this.edit_toggle());
this.container.addEventListener("click", () => this.editToggle());
this.container.appendChild(image);
}
reset(): void {
disable(): void {
const is_on = this.container.classList.contains("on");
if (is_on) {
this.edit_toggle();
this.editToggle();
}
}
edit_toggle(): void {
editToggle(): void {
const is_on = this.container.classList.contains("on");
if (is_on) {
this.container.classList.remove("on");
@ -35,7 +35,14 @@ export default class EditButton implements UiComponent {
$("root").classList.add("editor");
this.container.classList.add("on");
this.container.classList.remove("off");
this.cpu_signals.dispatch(UiCpuSignal.RequestProgramCounterChange, { address: 0 });
}
}
reset(): void {
this.disable();
}
softReset(): void {
this.disable();
}
}

View file

@ -3,10 +3,10 @@ import UiComponent from "../uiComponent";
export default class frequencyIndicator implements UiComponent {
container: HTMLElement;
private running: number | null = null;
private count: number = 0;
private last_value: number = 0;
private last_time: number = 0;
running: number | null = null;
count: number = 0;
last_value: number = 0;
last_time: number = 0;
events: UiEventHandler;
constructor(element: HTMLElement, events: UiEventHandler) {
this.container = element;
@ -19,16 +19,16 @@ export default class frequencyIndicator implements UiComponent {
if (this.running !== null) {
throw new Error("Tried starting frequencyIndicator twice!");
}
setInterval(this.update_indicator.bind(this), 1000);
window.setInterval(this.updateIndicator.bind(this), 1000);
}
stop(): void {
if (this.running === null) return;
clearInterval(this.running);
window.clearInterval(this.running);
this.running = null;
}
update_indicator(): void {
updateIndicator(): void {
const new_time = performance.now();
const dt = (new_time - this.last_time) / 1000 || 1;
const value = Math.round(this.count / dt);
@ -44,15 +44,21 @@ export default class frequencyIndicator implements UiComponent {
this.count = 0;
}
clock_cycle(): void {
clockCycle(): void {
this.count += 1;
}
reset(): void {
this.stop();
this.count = 0;
this.last_value = 0;
this.start();
}
softReset(): void {
this.reset();
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.Cycle, () => {
this.count += 1;

View file

@ -53,6 +53,7 @@ export default class MemoryView implements UiComponent {
});
}
this.events.listen(UiEvent.ChangeViewBank, ({ bank }) => this.setBank(bank));
this.setProgramCounter(0);
}
get program(): CelledViewer {
@ -76,6 +77,12 @@ export default class MemoryView implements UiComponent {
this.setProgramCounter(0);
}
softReset(): void {
for (const viewer of this.banks) viewer.clearAllClasses();
this.last_accessed_cell = null;
this.setProgramCounter(0);
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.MemoryAccessed, ({ address, bank, value }) => {
if (this.last_accessed_cell?.address !== address || this.last_accessed_cell?.bank !== bank) {

View file

@ -1,6 +1,7 @@
import { el } from "../../etc";
import { UiEventHandler, UiEvent, CpuEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import UiComponent from "../uiComponent";
import HoverTextBox from "../hoverTextBox";
const MAX_SLIDER = 1000;
@ -41,18 +42,22 @@ export default class pausePlay implements UiComponent {
this.events.listen(UiEvent.EditOn, () => this.disable());
this.events.listen(UiEvent.EditOff, () => this.enable());
const tb = new HoverTextBox(this.start_button, el("span").tx("hover test").st("color", "yellow").fin(), "left", 10);
tb.show();
}
disable(): void {
this.stop();
this.start_button.setAttribute("disabled", "true");
this.step_button.setAttribute("disabled", "true");
this.range.setAttribute("disabled", "true");
}
enable(): void {
this.start_button.removeAttribute("disabled");
this.step_button.removeAttribute("disabled");
this.range.removeAttribute("disabled");
}
@ -76,6 +81,7 @@ export default class pausePlay implements UiComponent {
};
loop();
}
private step(): void {
if (this.on) {
this.stop();
@ -98,4 +104,8 @@ export default class pausePlay implements UiComponent {
this.stop();
this.enable();
}
softReset(): void {
this.reset();
}
}

View file

@ -7,10 +7,7 @@ export default class RegisterView extends CelledViewer implements UiComponent {
events: UiEventHandler;
cpu_signals: UiCpuSignalHandler;
constructor(element: HTMLElement, events: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(8, 1, element, (address: u8, value: u8) => {
if (!isU3(address)) throw new Error("unreachable");
this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value });
});
super(8, 1, element, (a, v) => this.onEdit(a, v));
this.events = events;
this.cpu_signals = cpu_signals;
@ -24,8 +21,16 @@ export default class RegisterView extends CelledViewer implements UiComponent {
});
}
onEdit(address: u8, value: u8): void {
if (!isU3(address)) throw new Error("unreachable");
this.cpu_signals.dispatch(UiCpuSignal.RequestRegisterChange, { register_no: address as u3, value });
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.RegisterChanged, ({ register_no, value }) => this.setCellValue(register_no, value));
c.listen(CpuEvent.Reset, () => this.reset());
}
softReset(): void {
this.clearAllClasses();
}
}

View file

@ -1,5 +1,5 @@
import { el } from "../../etc";
import { UiEventHandler, UiCpuSignalHandler, UiEvent, UiCpuSignal } from "../../events";
import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import UiComponent from "../uiComponent";
export default class ResetButtons implements UiComponent {
@ -10,17 +10,22 @@ export default class ResetButtons implements UiComponent {
this.container = element;
this.events = events;
this.cpu_signals = cpu_signals;
const reset_button = el("button").cl("nostyle").tx("R").fin();
const trash_button = el("button").cl("nostyle").tx("T").fin();
const reset_button = el("button").cl("nostyle").ti("Reset State").tx("⟳").fin();
const trash_button = el("button").cl("nostyle").ti("Delete Code").tx("🗑").fin();
reset_button.addEventListener("click", () => this.resetClicked());
trash_button.addEventListener("click", () => this.trashClicked());
this.container.append(reset_button, trash_button);
}
resetClicked(): void {}
resetClicked(): void {
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuSoftReset);
}
trashClicked(): void {
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
const a = confirm("Clear all code? Irreversible");
if (a) {
this.cpu_signals.dispatch(UiCpuSignal.RequestCpuReset);
}
}
}

View file

@ -1,6 +1,6 @@
import { el } from "../../etc";
import { UiEventHandler, UiCpuSignalHandler, UiCpuSignal } from "../../events";
import { u2, u8, m256, isU2 } from "../../num";
import { u8, m256, isU2 } from "../../num";
import UiComponent from "../uiComponent";
export default class SaveLoad implements UiComponent {
@ -14,16 +14,16 @@ export default class SaveLoad implements UiComponent {
this.events = events;
this.cpu_signals = cpu_signals;
this.save_button = el("button").id("save_button").tx("Save").fin();
this.save_button = el("button").id("save_button").tx("").fin();
this.binary_upload = el("input")
.id("binary_upload")
.at("type", "file")
.at("name", "binary_upload")
.st("display", "none")
.fin();
const label = el("label").cl("button").at("for", "binary_upload").tx("Load Binary").fin();
const label = el("label").cl("button").at("for", "binary_upload").tx("").fin();
this.container.append(this.binary_upload, label, this.save_button);
this.container.append(this.save_button, this.binary_upload, label);
this.save_button.addEventListener("click", () => {
this.download();

48
src/ui/hoverTextBox.ts Normal file
View file

@ -0,0 +1,48 @@
import { el } from "../etc";
type RelativePosition = "top" | "bottom" | "left" | "right";
export default class HoverTextBox {
parent: HTMLElement;
position: RelativePosition;
gap: number;
contents: HTMLElement;
shown: false | { container: HTMLElement; resize_event_fn: () => void };
constructor(parent: HTMLElement, contents: HTMLElement, position: RelativePosition, gap: number) {
this.gap = gap;
this.position = position;
this.parent = parent;
this.contents = contents;
this.shown = false;
}
show(): void {
if (this.shown) return;
const container = el("div").st("position", "absolute").cl("hover_text_box").fin();
container.appendChild(this.contents);
const adjustBoxPosition = (cont: HTMLElement, parent: HTMLElement): void => {
const parent_x = parent.offsetLeft;
const parent_y = parent.offsetTop;
const style = window.getComputedStyle(cont);
const new_cont_x = parent_x;
const new_cont_y = parent_y + this.gap + cont.offsetHeight + 5;
cont.style.setProperty("top", `${new_cont_y}px`);
cont.style.setProperty("left", `${new_cont_x}px`);
};
document.body.appendChild(container);
adjustBoxPosition(container, this.parent);
const adjust_fn = adjustBoxPosition.bind(undefined, container, this.parent);
window.addEventListener("resize", adjust_fn);
this.shown = { container: container, resize_event_fn: adjust_fn };
}
hide(): void {
if (!this.shown) return;
this.shown.container.innerHTML = "";
this.shown.container.remove();
window.removeEventListener("resize", this.shown.resize_event_fn);
this.shown = false;
}
}

View file

@ -12,7 +12,7 @@ export default abstract class WindowBox {
title_bar: HTMLElement;
readonly title: string;
private collapse_button: HTMLButtonElement;
private collapsed = false;
private is_collapsed = false;
private fit_content = false;
private resize?: HTMLElement;
private resize_func?: (e: MouseEvent) => void;
@ -44,7 +44,7 @@ export default abstract class WindowBox {
} else {
this.resize = el("div").id("resize").fin();
this.container.appendChild(this.resize);
this.resize_func = this.resize_move.bind(this);
this.resize_func = this.resizeMove.bind(this);
this.resize.addEventListener("mousedown", (e) => {
if (this.resize_func) window.addEventListener("mousemove", this.resize_func);
});
@ -58,7 +58,7 @@ export default abstract class WindowBox {
this.removeResizeListeners();
if (this.resize) this.resize.style.visibility = "hidden";
this.setHeight(this.title_bar.offsetHeight - BORDER_STROKE);
this.collapsed = true;
this.is_collapsed = true;
}
correctHeightValue(height: number): number {
@ -74,7 +74,7 @@ export default abstract class WindowBox {
}
toggleCollapse(): void {
if (this.collapsed) {
if (this.is_collapsed) {
this.uncollapse();
} else {
this.collapse();
@ -91,15 +91,15 @@ export default abstract class WindowBox {
const new_height = this.correctHeightValue(this.title_bar.offsetHeight + 200);
this.setHeight(new_height);
this.collapsed = false;
this.is_collapsed = false;
}
removeResizeListeners(): void {
if (this.resize_func) window.removeEventListener("mousemove", this.resize_func);
}
resize_move(e: MouseEvent): void {
if (this.collapsed) {
resizeMove(e: MouseEvent): void {
if (this.is_collapsed) {
this.uncollapse();
this.removeResizeListeners();
return;
@ -111,4 +111,8 @@ export default abstract class WindowBox {
}
this.setHeight(e.clientY - this.container.offsetTop + window.scrollY);
}
isCollapsed(): boolean {
return this.is_collapsed;
}
}

View file

@ -32,4 +32,8 @@ export default class BankVisualizer extends WindowBox implements UiComponent {
this.cpu_banks[0].setAttribute("stroke", "yellow");
}
softReset(): void {
this.reset();
}
}

View file

@ -67,4 +67,8 @@ export default class InstructionExplainer extends WindowBox implements UiCompone
reset(): void {
this.container.querySelectorAll("#expl_box").forEach((e) => e.remove());
}
softReset(): void {
this.reset();
}
}

View file

@ -24,4 +24,8 @@ export default class Printout extends WindowBox implements UiComponent {
reset(): void {
this.text_box.textContent = "";
}
softReset(): void {
this.reset();
}
}

View file

@ -15,6 +15,7 @@ export default class Screen extends WindowBox implements UiComponent {
ctx: CanvasRenderingContext2D;
scale: number;
current_vram_bank: u2 = DEFAULT_VRAM_BANK;
constructor(element: HTMLElement, event: UiEventHandler, cpu_signals: UiCpuSignalHandler) {
super(element, "TV", { collapsed: true, fit_content: true });
this.cpu_signals = cpu_signals;
@ -25,27 +26,31 @@ export default class Screen extends WindowBox implements UiComponent {
this.screen.width = CANVAS_SIZE;
this.screen.height = CANVAS_SIZE;
const ctx = this.screen.getContext("2d");
if (ctx === null) {
throw new Error("could not load screen");
}
if (ctx === null) throw new Error("could not load screen");
this.ctx = ctx;
this.container.appendChild(this.screen);
this.test_pattern();
this.renderTestPattern();
}
private test_pattern(): void {
private renderTestPattern(): void {
for (let x = 0; x < 256; x++) {
this.setPixel(x as u8, x as u8);
}
}
reset(): void {
const ctx = this.screen.getContext("2d");
if (ctx === null) {
throw new Error("todo");
for (let i = 0; i < 256; i++) {
this.setPixel(i as u8, 0);
}
}
softReset(): void {
this.reset();
}
initCpuEvents(c: CpuEventHandler): void {
c.listen(CpuEvent.MemoryChanged, ({ address, bank, value }) => {
if (bank !== 1) return;
@ -68,6 +73,8 @@ export default class Screen extends WindowBox implements UiComponent {
const y = Math.floor(address / 16) as u4;
const point: [number, number] = [x * this.scale, y * this.scale];
// TODO, come up with better color scheme.
// Probable a lookup table
const RED_SCALE = 255 / 2 ** 3;
const GREEN_SCALE = 255 / 2 ** 3;
const BLUE_SCALE = 255 / 2 ** 2;

View file

@ -34,6 +34,13 @@ class ElementInProgress<E extends HTMLElement> {
return this;
}
/** Set title */
ti(title: string): ElementInProgress<E> {
this.element.title = title;
return this;
}
/** Return created element */
fin(): E {
return this.element;
}

View file

@ -29,7 +29,9 @@ Limit size to printout text buffer
Improve instruction explainer. Clearly show what is an instruction and what is a parameter
Ui showing CPU flag(s) (Carry)
Ui showing CPU flag(s) (Carry) and call stack
Share programs from encoded url
Responsive layout
@ -37,4 +39,4 @@ standardize names of all things
Documentation with standard names
Example Programs
Example Programs (loaded as one of those encoded string url things)