From b2adf1f9dcb926b3a5f436d35ca28aeb519e911b Mon Sep 17 00:00:00 2001 From: Guy Perfect Date: Mon, 30 Aug 2021 02:14:06 +0000 Subject: [PATCH] Adding Memory window --- app/App.js | 110 +++++++++++++---- app/Debugger.js | 43 +++++++ app/Emulator.js | 79 ++++++++++++ app/_boot.js | 5 +- app/locale/en-US.js | 8 +- app/theme/dark.css | 6 +- app/theme/kiosk.css | 90 +++++++++----- app/theme/light.css | 6 +- app/theme/virtual.css | 4 +- app/toolkit/Component.js | 21 +++- app/toolkit/Label.js | 52 ++++++++ app/toolkit/Menu.js | 9 ++ app/toolkit/Panel.js | 32 +++-- app/toolkit/Window.js | 87 ++++++++++++-- app/windows/MemoryWindow.js | 232 ++++++++++++++++++++++++++++++++++++ core/bus.c | 119 ++++++++++++++++++ core/vb.c | 196 ++++++++++++++++++++++++++++++ core/vb.h | 134 +++++++++++++++++++++ makefile | 25 ++-- wasm/wasm.c | 98 +++++++++++++++ 20 files changed, 1260 insertions(+), 96 deletions(-) create mode 100644 app/Debugger.js create mode 100644 app/Emulator.js create mode 100644 app/toolkit/Label.js create mode 100644 app/windows/MemoryWindow.js create mode 100644 core/bus.c create mode 100644 core/vb.c create mode 100644 core/vb.h create mode 100644 wasm/wasm.c diff --git a/app/App.js b/app/App.js index 4d6fe1e..c676c02 100644 --- a/app/App.js +++ b/app/App.js @@ -3,8 +3,49 @@ // Top-level state and UI manager globalThis.App = class App { - // Object constructor - constructor() { + // Produce a new class instance + static async create() { + let ret = new App(); + await ret.init(); + return ret; + } + + // Perform initial tasks + async init() { + + // Initialization tasks + this.initEnv(); + this.initMenus(); + + // WebAssembly core module + this.core = new Worker(Bundle.get("app/Emulator.js").toDataURL()); + await new Promise((resolve,reject)=>{ + this.core.onmessage = e=>{ + this.core.onmessage = e=>this.onmessage(e.data); + resolve(); + }; + this.core.postMessage({ + command: "Init", + wasm : Bundle.get("core.wasm").data.buffer + }); + }); + + // Desktop pane + this.desktop = this.gui.newPanel({ layout: "desktop" }); + this.desktop.setRole("group"); + this.desktop.element.setAttribute("desktop", ""); + this.gui.add(this.desktop); + + // Debugging windows + this.debuggers = [ + new Debugger(this, 0), + new Debugger(this, 1) + ]; + + } + + // Initialize environment settings + initEnv() { // Configure themes Bundle.get("app/theme/kiosk.css").style(); @@ -29,6 +70,10 @@ globalThis.App = class App { // Configure locales this.gui.addLocale(Bundle.get("app/locale/en-US.js").toString()); this.gui.setLocale(navigator.language); + } + + // Initialize main menu bar + initMenus() { // Menu bar this.mainMenu = this.gui.newMenuBar({ name: "{menu._}" }); @@ -40,27 +85,41 @@ globalThis.App = class App { let item = menu.newMenuItem({ text: "{menu.file.loadROM}"}); item.addClickListener(()=>this.loadROM()); + // Debug menu + menu = this.mainMenu.newMenu({ text: "{menu.debug._}" }); + item = menu.newMenuItem({ text: "{memory._}" }); + item.addClickListener( + ()=>this.debuggers[0].memory.setVisible(true, true)); + // Theme menu - menu = this.mainMenu.newMenu({ text: "{menu.theme._}"}); - item = menu.newMenuItem({ text: "{menu.theme.light}"}); + menu = this.mainMenu.newMenu({ text: "{menu.theme._}" }); + item = menu.newMenuItem({ text: "{menu.theme.light}" }); item.addClickListener(()=>this.setTheme("light")); - item = menu.newMenuItem({ text: "{menu.theme.dark}"}); + item = menu.newMenuItem({ text: "{menu.theme.dark}" }); item.addClickListener(()=>this.setTheme("dark")); - item = menu.newMenuItem({ text: "{menu.theme.virtual}"}); + item = menu.newMenuItem({ text: "{menu.theme.virtual}" }); item.addClickListener(()=>this.setTheme("virtual")); + } - // Desktop pane - let desktop = this.gui.newPanel({ layout: "desktop" }); - desktop.setRole("group"); - desktop.element.setAttribute("desktop", ""); - this.gui.add(desktop); - let wnd = this.gui.newWindow({ - title: "{memory._}" - }); - desktop.add(wnd); - wnd.setLocation ( 20, 10); - wnd.setClientSize(384, 224); + ///////////////////////////// Message Methods ///////////////////////////// + + // Message received + onmessage(msg) { + if ("debug" in msg) { + this.debuggers[msg.sim].message(msg); + return; + } + switch (msg.command) { + case "SetROM": this.setROM(msg); break; + } + } + + // ROM buffer has been configured + setROM(msg) { + let dbg = this.debuggers[msg.sim]; + dbg.memory.setVisible(true, true); + dbg.refresh(); } @@ -71,12 +130,12 @@ globalThis.App = class App { loadROM() { let file = document.createElement("input"); file.type = "file"; - file.addEventListener("input", ()=>this.setROM(file.files[0])); + file.addEventListener("input", ()=>this.selectROM(file.files[0])); file.click(); } // Specify a ROM file - async setROM(file) { + async selectROM(file) { // No file is specified (perhaps the user canceled) if (file == null) @@ -94,18 +153,17 @@ globalThis.App = class App { // Load the file data into a byte buffer let filename = file.name; - try { file = new Uint8Array(await file.arrayBuffer()); } + try { file = await file.arrayBuffer(); } catch { alert(this.gui.translate("{app.readFileError}")); return; } - // Testing output pending further features - alert(this.gui.translate("{app.romLoaded}", { - filename: filename, - size : file.length + " byte" + (file.length == 1 ? "" : "s") - })); - + this.core.postMessage({ + command: "SetROM", + rom : file, + sim : 0 + }); } // Specify the current color theme diff --git a/app/Debugger.js b/app/Debugger.js new file mode 100644 index 0000000..846aaf9 --- /dev/null +++ b/app/Debugger.js @@ -0,0 +1,43 @@ +"use strict"; + +// Debugging UI manager +globalThis.Debugger = class Debugger { + + // Object constructor + constructor(app, sim) { + + // Configure instance fields + this.app = app; + this.core = app.core; + this.gui = app.gui; + this.sim = sim; + + // Configure Memory window + this.memory = new MemoryWindow(this, { + title : "{sim}{memory._}", + center : true, + height : 300, + visible: false, + width : 400 + }); + this.memory.addCloseListener(e=>this.memory.setVisible(false)); + app.desktop.add(this.memory); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Message received from emulation thread + message(msg) { + switch (msg.debug) { + case "Memory": this.memory.message(msg); break; + } + } + + // Reload all output + refresh() { + this.memory.refresh(); + } + +}; diff --git a/app/Emulator.js b/app/Emulator.js new file mode 100644 index 0000000..b087062 --- /dev/null +++ b/app/Emulator.js @@ -0,0 +1,79 @@ +"use strict"; + +// Worker that manages a WebAssembly instance of the C core library +(globalThis.Emulator = class Emulator { + + // Static initializer + static initializer() { + new Emulator(); + } + + // Object constructor + constructor() { + + // Configure message port + onmessage = e=>this.onmessage(e.data); + } + + + + ///////////////////////////// Message Methods ///////////////////////////// + + // Message received + onmessage(msg) { + switch (msg.command) { + case "Init" : this.init (msg); break; + case "ReadBuffer": this.readBuffer(msg); break; + case "SetROM" : this.setROM (msg); break; + } + } + + // Initialize the WebAssembly core module + async init(msg) { + + // Load and instantiate the WebAssembly module + this.wasm = await WebAssembly.instantiate(msg.wasm, + { env: { emscripten_notify_memory_growth: function(){} }}); + this.wasm.instance.exports.Init(); + + // Configure instance fields + this.core = this.wasm.instance.exports; + this.memory = this.core.memory.buffer; + + postMessage({ command: "Init" }); + } + + // Read multiple data units from the bus + readBuffer(msg) { + let buffer = this.malloc(Uint8Array, msg.size); + this.core.ReadBuffer(msg.sim, buffer.pointer, msg.address, msg.size); + msg.buffer = buffer.buffer.slice( + buffer.pointer, buffer.pointer + msg.size); + this.core.Free(buffer.pointer); + postMessage(msg, msg.buffer); + } + + // Supply a ROM buffer + setROM(msg) { + let rom = new Uint8Array(msg.rom); + let buffer = this.malloc(Uint8Array, rom.length); + for (let x = 0; x < rom.length; x++) + buffer[x] = rom[x]; + msg.success = !!this.core.SetROM(msg.sim, buffer.pointer, rom.length); + delete msg.rom; + postMessage(msg); + } + + + + ///////////////////////////// Utility Methods ///////////////////////////// + + // Allocate a typed array in WebAssembly core memory + malloc(type, size) { + let pointer = this.core.Malloc(size); + let ret = new type(this.memory, pointer, size); + ret.pointer = pointer; + return ret; + } + +}).initializer(); diff --git a/app/_boot.js b/app/_boot.js index 72ea4f9..bd7c912 100644 --- a/app/_boot.js +++ b/app/_boot.js @@ -285,15 +285,18 @@ for (let file of manifest) { // Program startup let run = async function() { await Bundle.run("app/App.js"); + await Bundle.run("app/Debugger.js"); await Bundle.run("app/toolkit/Toolkit.js"); await Bundle.run("app/toolkit/Component.js"); await Bundle.run("app/toolkit/Panel.js"); await Bundle.run("app/toolkit/Application.js"); await Bundle.run("app/toolkit/Button.js"); + await Bundle.run("app/toolkit/Label.js"); await Bundle.run("app/toolkit/MenuBar.js"); await Bundle.run("app/toolkit/MenuItem.js"); await Bundle.run("app/toolkit/Menu.js"); await Bundle.run("app/toolkit/Window.js"); - new App(); + await Bundle.run("app/windows/MemoryWindow.js"); + await App.create(); }; run(); diff --git a/app/locale/en-US.js b/app/locale/en-US.js index 370f128..dc506e1 100644 --- a/app/locale/en-US.js +++ b/app/locale/en-US.js @@ -4,6 +4,7 @@ app : { close : "Close", console : "Console", + goto_ : "Enter the address to seek to:", romLoaded : "Successfully loaded file \"{filename}\" ({size})", romNotVB : "The selected file is not a Virtual Boy ROM.", readFileError: "Unable to read the selected file." @@ -12,8 +13,11 @@ _: "Memory" }, menu: { - _ : "Main application menu", - file: { + _ : "Main application menu", + debug: { + _: "Debug", + }, + file : { _ : "File", loadROM: "Load ROM..." }, diff --git a/app/theme/dark.css b/app/theme/dark.css index 5fa27e9..94b3087 100644 --- a/app/theme/dark.css +++ b/app/theme/dark.css @@ -2,11 +2,13 @@ --control : #222222; --control-focus : #444444; --control-shadow : #999999; + --control-text : #cccccc; --desktop : #111111; - --text : #cccccc; --window-blur : #555555; --window-blur-text : #cccccc; - --window-close-text: #cccccc; + --window-close : #ee9999; + --window-close-blur: #998080; + --window-close-text: #ffffff; --window-focus : #007ACC; --window-focus-text: #ffffff; } \ No newline at end of file diff --git a/app/theme/kiosk.css b/app/theme/kiosk.css index 555cb67..b5f7429 100644 --- a/app/theme/kiosk.css +++ b/app/theme/kiosk.css @@ -1,9 +1,15 @@ /* Common styles for all themes */ :root { - color : var(--text); - font-family: Arial, sans-serif; - font-size : 12px; + --font-dialog: Arial, sans-serif; + --font-hex : Consolas, monospace; + --font-size : 12; +} + +:root { + color : var(--control-text); + font-family: var(--font-dialog); + font-size : calc(1px * var(--font-size)); } body { @@ -47,13 +53,14 @@ body { [role="menubar"] { background : var(--control); - border-bottom: 1px solid var(--text); + border-bottom: 1px solid var(--control-text); padding : 2px 3px 3px 2px; } [role="menubar"] [role="menuitem"] { - margin : 1px; - padding: 3px; + line-height: 1em; + margin : 1px; + padding : 3px; } [role="menubar"] [role="menuitem"]:focus { @@ -77,8 +84,8 @@ body { [role="menubar"] [role="menu"] { background: var(--control); box-shadow: - 0 0 0 1px var(--text), - 1px 1px 0 1px var(--text) + 0 0 0 1px var(--control-text), + 1px 1px 0 1px var(--control-text) ; min-height: 16px; min-width : 16px; @@ -97,13 +104,14 @@ body { background: var(--control); box-shadow: 0 0 0 1px var(--control), - 0 0 0 2px var(--text), - 1px 1px 0 2px var(--text) + 0 0 0 2px var(--control-text), + 1px 1px 0 2px var(--control-text) ; row-gap : 3px; } [role="dialog"] [name="title-bar"] { + min-height: calc(1em + 5px); } [role="dialog"][focus="true"] [name="title-bar"] { @@ -125,13 +133,14 @@ body { [role="dialog"] [name="title-icon"], [role="dialog"] [name="title-close-box"] { align-self : stretch; - min-width : calc(1em + 4px); - width : calc(1em + 4px); + min-width : calc(1em + 5px); + width : calc(1em + 5px); } [role="dialog"] [name="title"] { color : var(--window-focus-text); font-weight : bold; + line-height : calc(1em + 1px); overflow : hidden; padding : 2px; text-align : center; @@ -140,28 +149,29 @@ body { } [role="dialog"][focus="false"] [name="title"] { - color : var(--window-blur-text); + color: var(--window-blur-text); } [role="dialog"] [name="title-close-box"] { align-items : center; display : flex; - justify-content: flex-start; + justify-content: center; } [role="dialog"] [name="title-close"] { align-items : center; - background : var(--control); - box-shadow : - 0 0 0 1px var(--control), - 0 0 0 2px var(--control-shadow) - ; + background : var(--window-close); + box-shadow : 0 0 0 1px var(--control-shadow); display : flex; - height : 11px; + height : 13px; justify-content: center; overflow : hidden; padding : 0; - width : 11px; + width : 13px; +} + +[role="dialog"][focus="false"] [name="title-close"] { + background: var(--window-close-blur); } [role="dialog"] [name="title-close"]:focus { @@ -169,20 +179,44 @@ body { } [role="dialog"] [name="title-close"][active] { - box-shadow : - -1px -1px 0 1px var(--control), - -1px -1px 0 2px var(--control-shadow) - ; - margin: 2px 0 0 2px; + box-shadow: 0 0 0 1px var(--control-shadow); + margin : 0; } [role="dialog"] [name="title-close"]:after { color : var(--window-close-text); content : '\00d7'; - font-size : 12px; - line-height: 1em; + font-size : 13px; + line-height: 13px; +} + +[role="dialog"] [name="title-close"][active]:after { + color : var(--window-close-text); + content : '\00d7'; + font-size : 13px; + line-height: 13px; + margin : 1px -1px -1px 1px; } [role="dialog"] [name="client"] { background: var(--control); } + + + +/******************************* Memory Window *******************************/ + +[window="memory"] [name="client"] { + align-items: center; + column-gap : calc(1px * var(--font-size) / 2); +} + +[window="memory"] [name="address"], +[window="memory"] [name="byte"] { + font-family: var(--font-hex); + line-height: 1em; +} + +[window="memory"] [name="address"] { + align-self: start; +} diff --git a/app/theme/light.css b/app/theme/light.css index 58a4cd8..a07fd6a 100644 --- a/app/theme/light.css +++ b/app/theme/light.css @@ -2,11 +2,13 @@ --control : #eeeeee; --control-focus : #cccccc; --control-shadow : #999999; + --control-text : #000000; --desktop : #cccccc; - --text : #000000; --window-blur : #cccccc; --window-blur-text : #444444; - --window-close-text: #444444; + --window-close : #ee9999; + --window-close-blur: #d4c4c4; + --window-close-text: #ffffff; --window-focus : #80ccff; --window-focus-text: #000000; } diff --git a/app/theme/virtual.css b/app/theme/virtual.css index ffbc469..dd41b56 100644 --- a/app/theme/virtual.css +++ b/app/theme/virtual.css @@ -2,10 +2,12 @@ --control : #000000; --control-focus : #550000; --control-shadow : #aa0000; + --control-text : #ff0000; --desktop : #000000; - --text : #ff0000; --window-blur : #000000; --window-blur-text : #aa0000; + --window-close : #aa0000; + --window-close-blur: #550000; --window-close-text: #ff0000; --window-focus : #550000; --window-focus-text: #ff0000; diff --git a/app/toolkit/Component.js b/app/toolkit/Component.js index d04826e..0365bff 100644 --- a/app/toolkit/Component.js +++ b/app/toolkit/Component.js @@ -39,11 +39,6 @@ Toolkit.Component = class Component { this.resizeListeners.push(listener); } - // Remove the component from its parent - remove() { - this.parent && this.parent.remove(this); - } - // Retrieve the bounding box of the element getBounds() { return this.element.getBoundingClientRect(); @@ -62,6 +57,14 @@ Toolkit.Component = class Component { return true; } + // Specify the location and size of the component + setBounds(left, top, width, height) { + this.setLeft (left ); + this.setTop (top ); + this.setWidth (width ); + this.setHeight(height); + } + // Specify the display CSS property of the visible element setDisplay(display) { this.display = display || null; @@ -90,6 +93,12 @@ Toolkit.Component = class Component { this.setTop (top ); } + // Specify a localization property value + setProperty(key, value) { + this.properties[key] = value; + this.localize(); + } + // Specify both the width and the height of the component setSize(width, height) { this.setHeight(height); @@ -146,7 +155,7 @@ Toolkit.Component = class Component { onresized() { let bounds = this.getBounds(); for (let listener of this.resizeListeners) - listener(bounds, this); + listener(bounds); } }; diff --git a/app/toolkit/Label.js b/app/toolkit/Label.js new file mode 100644 index 0000000..7703e8e --- /dev/null +++ b/app/toolkit/Label.js @@ -0,0 +1,52 @@ +"use strict"; + +// Display text component +Toolkit.Label = class Label extends Toolkit.Component { + + // Object constructor + constructor(application, options) { + super(application, "div", options); + options = options || {}; + + // Configure instance fields + this.localized = "localized" in options ? !!options.localized : false; + this.text = options.text || ""; + + // Configure element + this.element.style.cursor = "default"; + this.element.style.userSelect = "none"; + + // Configure properties + this.setText(this.text); + if (this.localized) + this.application.addComponent(this); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // Retrieve the label's display text + getText() { + return this.text; + } + + // Specify the label's display text + setText(text) { + this.text = text || ""; + this.localize(); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update display text with localized strings + localize() { + let text = this.text; + if (this.localized && this.application) + text = this.application.translate(text, this); + this.element.innerText = text; + } + +}; diff --git a/app/toolkit/Menu.js b/app/toolkit/Menu.js index bd5647f..ad39ded 100644 --- a/app/toolkit/Menu.js +++ b/app/toolkit/Menu.js @@ -17,6 +17,9 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem { this.menu.element.style.position = "absolute"; this.menu.element.setAttribute("role", "menu"); this.menu.element.setAttribute("aria-labelledby", this.id); + this.menu.element.addEventListener("pointerdown",e=>this.stopEvent(e)); + this.menu.element.addEventListener("pointermove",e=>this.stopEvent(e)); + this.menu.element.addEventListener("pointerup" ,e=>this.stopEvent(e)); this.containers.push(this.menu); this.children = this.menu.children; @@ -213,4 +216,10 @@ Toolkit.Menu = class Menu extends Toolkit.MenuItem { e.stopPropagation(); } + // Prevent an event from bubbling + stopEvent(e) { + e.preventDefault(); + e.stopPropagation(); + } + }; diff --git a/app/toolkit/Panel.js b/app/toolkit/Panel.js index 41c93de..c7f06b5 100644 --- a/app/toolkit/Panel.js +++ b/app/toolkit/Panel.js @@ -6,9 +6,9 @@ Toolkit.Panel = class Panel extends Toolkit.Component { // Object constructor constructor(application, options) { super(application, "div", options); + options = options || {}; // Configure instance fields - options = options || {}; this.alignCross = "start"; this.alignMain = "start"; this.application = application; @@ -22,8 +22,10 @@ Toolkit.Panel = class Panel extends Toolkit.Component { this.wrap = false; // Configure element - this.element.style.minHeight = "0"; - this.element.style.minWidth = "0"; + if ("noShrink" in options ? !options.noShrink : true) { + this.element.style.minHeight = "0"; + this.element.style.minWidth = "0"; + } this.setOverflow(this.overflowX, this.overflowY); // Configure layout @@ -57,6 +59,11 @@ Toolkit.Panel = class Panel extends Toolkit.Component { return new Toolkit.Button(this.application, options); } + // Create a Label and associate it with the application + newLabel(options) { + return new Toolkit.Label(this.application, options); + } + // Create a MenuBar and associate it with the application newMenuBar(options) { return new Toolkit.MenuBar(this.application, options); @@ -141,6 +148,16 @@ Toolkit.Panel = class Panel extends Toolkit.Component { + ///////////////////////////// Package Methods ///////////////////////////// + + // Move a window to the foreground + bringToFront(wnd) { + for (let child of this.children) + child.element.style.zIndex = child == wnd ? "0" : "1"; + } + + + ///////////////////////////// Private Methods ///////////////////////////// // Resize event handler @@ -152,12 +169,13 @@ Toolkit.Panel = class Panel extends Toolkit.Component { // Ensure all child windows are visible in the viewport for (let wnd of this.children) { + if (!wnd.isVisible()) + continue; let bounds = wnd.getBounds(); wnd.contain( - desktop, - bounds, bounds.x - desktop.x, - bounds.y - desktop.y + bounds.y - desktop.y, + desktop, bounds ); } @@ -244,8 +262,6 @@ Toolkit.Panel = class Panel extends Toolkit.Component { this.element.style.removeProperty("grid-template-rows" ); this.element.style.removeProperty("justify-content" ); this.element.style.removeProperty("flex-direction" ); - this.element.style.removeProperty("overflow-x" ); - this.element.style.removeProperty("overflow-y" ); } }; diff --git a/app/toolkit/Window.js b/app/toolkit/Window.js index d85ced1..e891665 100644 --- a/app/toolkit/Window.js +++ b/app/toolkit/Window.js @@ -9,11 +9,16 @@ Toolkit.Window = class Window extends Toolkit.Panel { options = options || {}; // Configure instance fields - this.dragBounds = null; - this.dragCursor = { x: 0, y: 0 }; - this.dragEdge = null; - this.lastFocus = this.element; - this.title = options.title || ""; + this.closeListeners = []; + this.dragBounds = null; + this.dragCursor = { x: 0, y: 0 }; + this.dragEdge = null; + this.initialCenter = "center" in options ? !!options.center : false; + this.initialHeight = options.height || 64; + this.initialWidth = options.width || 64; + this.lastFocus = this.element; + this.shown = this.visible; + this.title = options.title || ""; // Configure element this.setLayout("flex", { @@ -23,6 +28,7 @@ Toolkit.Window = class Window extends Toolkit.Panel { overflowY : "visible" }); this.setRole("dialog"); + this.setBounds(0, 0, 64, 64); this.element.style.position = "absolute"; this.element.setAttribute("aria-modal", "false"); this.element.setAttribute("focus" , "false"); @@ -53,6 +59,7 @@ Toolkit.Window = class Window extends Toolkit.Panel { layout : "flex", alignCross: "center", direction : "row", + noShrink : true, overflowX : "visible", overflowY : "visible" })); @@ -64,7 +71,7 @@ Toolkit.Window = class Window extends Toolkit.Panel { this.titleIcon.element.style.removeProperty("min-width"); // Configure title text element - this.titleElement = this.titleBar.add(this.newPanel({})); + this.titleElement = this.titleBar.add(this.newLabel({})); this.titleElement.element.setAttribute("name", "title"); this.titleElement.element.style.cursor = "default"; this.titleElement.element.style.flexGrow = "1"; @@ -81,9 +88,13 @@ Toolkit.Window = class Window extends Toolkit.Panel { toolTip : "{app.close}" })); this.titleClose.element.setAttribute("name", "title-close"); + this.titleClose.addClickListener(e=>this.onclose(e)); // Configure client area - this.client = this.body.add(this.newPanel({})); + this.client = this.body.add(this.newPanel({ + overflowX: "hidden", + overflowY: "hidden" + })); this.client.element.style.flexGrow = "1"; this.client.element.setAttribute("name", "client"); this.client.element.addEventListener( @@ -91,6 +102,8 @@ Toolkit.Window = class Window extends Toolkit.Panel { // Configure properties this.setTitle(this.title); + if (this.shown) + this.setClientSize(this.initialHeight, this.initialWidth); application.addComponent(this); } @@ -98,6 +111,12 @@ Toolkit.Window = class Window extends Toolkit.Panel { ///////////////////////////// Public Methods ////////////////////////////// + // Add a callback for close events + addCloseListener(listener) { + if (this.closeListeners.indexOf(listener) == -1) + this.closeListeners.push(listener); + } + // Specify the size of the client rectangle in pixels setClientSize(width, height) { let bounds = this.getBounds(); @@ -114,6 +133,19 @@ Toolkit.Window = class Window extends Toolkit.Panel { this.localize(); } + // Specify whether the component is visible + setVisible(visible, focus) { + super.setVisible(visible); + if (this.client === undefined) + return; + if (visible) + this.contain(); + if (focus) + this.focus(); + if (!this.shown) + this.firstShow(); + } + ///////////////////////////// Package Methods ///////////////////////////// @@ -123,6 +155,7 @@ Toolkit.Window = class Window extends Toolkit.Panel { if (this.lastFocus != this) this.lastFocus.focus(); else this.element.focus(); + this.parent.bringToFront(this); } @@ -130,9 +163,14 @@ Toolkit.Window = class Window extends Toolkit.Panel { ///////////////////////////// Private Methods ///////////////////////////// // Position the window using a tentative location in the desktop - contain(desktop, bounds, x, y, client) { - bounds = bounds || this.getBounds(); - client = client || this.client.getBounds(); + contain(x, y, desktop, bounds, client) { + desktop = desktop || this.parent.getBounds(); + bounds = bounds || this.getBounds(); + client = client || this.client.getBounds(); + if (x === undefined) + x = bounds.x - desktop.x; + if (y === undefined) + y = bounds.y - desktop.y; // Restrict window position x = Math.min(x, desktop.width - 16); @@ -172,6 +210,25 @@ Toolkit.Window = class Window extends Toolkit.Panel { return null; } + // The window is being displayed for the first time + firstShow() { + this.shown = true; + + // Configure the initial size of the window + this.setClientSize(this.initialWidth, this.initialHeight); + + // Configure the initial position of the window + if (!this.initialCenter) + return; + let bounds = this.getBounds(); + let desktop = this.parent.getBounds(); + this.contain( + Math.floor((desktop.width - bounds.width ) / 2), + Math.ceil ((desktop.height - bounds.height) / 2), + desktop, bounds + ); + } + // Update display text with localized strings localize() { let title = this.title; @@ -193,6 +250,12 @@ Toolkit.Window = class Window extends Toolkit.Panel { this.focus(); } + // Window close + onclose(e) { + for (let listener of this.closeListeners) + listener(e); + } + // Focus gained event capture onfocus(e) { @@ -284,11 +347,9 @@ Toolkit.Window = class Window extends Toolkit.Panel { // Move the window if (this.dragEdge == null) { this.contain( - desktop, - bounds, this.dragBounds.x - desktop.x + rX, this.dragBounds.y - desktop.y + rY, - client + desktop, bounds, client ); return; } diff --git a/app/windows/MemoryWindow.js b/app/windows/MemoryWindow.js new file mode 100644 index 0000000..b8fdf89 --- /dev/null +++ b/app/windows/MemoryWindow.js @@ -0,0 +1,232 @@ +"use strict"; + +// Hex editor style memory viewer +globalThis.MemoryWindow = class MemoryWindow extends Toolkit.Window { + + // Object constructor + constructor(debug, options) { + super(debug.gui, options); + + // Configure instance fields + this.address = 0xFFFFFFF0; + this.debug = debug; + this.rows = [new MemoryWindow.Row(this.client)]; + + // Configure element + this.element.setAttribute("window", "memory"); + + // Configure client + this.client.setLayout("grid", { columns: "repeat(19, max-content)" }); + this.client.element.style.gridAutoRows = "max-content"; + this.client.setOverflow("auto", "hidden"); + this.client.addResizeListener(b=>this.refresh(b.height)); + this.client.element.addEventListener("keydown", e=>this.onkeydown(e)); + this.client.element.addEventListener("wheel", e=>this.onwheel(e)); + + // Configure properties + this.setProperty("sim", ""); + } + + + + ///////////////////////////// Public Methods ////////////////////////////// + + // The window is being displayed for the first time + firstShow() { + super.firstShow(); + this.seek(this.address, Math.floor(this.lines(true) / 3)); + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Update the display with current emulation data + refresh(clientHeight, lineHeight) { + clientHeight = clientHeight || this.client.getBounds().height; + lineHeight = lineHeight || this.lineHeight(); + let rowCount = this.lines(false, clientHeight, lineHeight); + + // Showing for the first time + if (this.address < 0) + this.seek(-this.address, Math.floor(rowCount / 3)); + + // Request bus data from the WebAssembly core + this.debug.core.postMessage({ + command: "ReadBuffer", + debug : "Memory", + address: this.address, + sim : this.debug.sim, + size : rowCount * 16 + }); + + // Configure elements + for (let y = this.rows.length; y < rowCount; y++) + this.rows[y] = new MemoryWindow.Row(this.client); + for (let y = rowCount; y < this.rows.length; y++) + this.rows[y].remove(); + if (this.rows.length > rowCount) + this.rows.splice(rowCount, this.rows.length - rowCount); + } + + + + ///////////////////////////// Message Methods ///////////////////////////// + + // Message received + message(msg) { + switch (msg.command) { + case "ReadBuffer": this.readBuffer(msg); break; + } + } + + // Received bytes from the bus + readBuffer(msg) { + let bytes = new Uint8Array(msg.buffer); + for ( + let x = 0, address = this.address, offset = 0; + x < this.rows.length && offset < bytes.length; + x++, address = (address + 16 & 0xFFFFFFFF) >>> 0, offset += 16 + ) this.rows[x].update(address, bytes, offset); + } + + + + ///////////////////////////// Private Methods ///////////////////////////// + + // Determine the height in pixels of one row of output + lineHeight() { + let ret = this.rows[0].address.getBounds().height; + return ret; + } + + // Determine the number of rows of output + lines(fullyVisible, clientHeight, lineHeight) { + clientHeight = clientHeight || this.client.getBounds().height; + lineHeight = lineHeight || this.lineHeight(); + let ret = clientHeight / lineHeight; + ret = fullyVisible ? Math.floor(ret) : Math.ceil(ret); + return Math.max(1, Math.min(ret, Math.floor(clientHeight / 8))); + } + + // Key down event handler + onkeydown(e) { + + // Control is pressed + if (e.ctrlKey) switch (e.key) { + case "g": case "G": + let addr = prompt(this.application.translate("{app.romLoaded}")); + if (addr === null) + break; + this.seek( + (parseInt(addr, 16) & 0xFFFFFFF0) >>> 0, + Math.floor(this.lines(true) / 3) + ); + this.refresh(); + break; + default: return; + } + + // Processing by key + else switch (e.key) { + + case "ArrowDown": + this.address = (this.address + 16 & 0xFFFFFFFF) >>> 0; + this.refresh(); + break; + + case "ArrowUp": + this.address = (this.address - 16 & 0xFFFFFFFF) >>> 0; + this.refresh(); + break; + + case "PageUp": + this.address = (this.address - 16 * this.lines(true) & + 0xFFFFFFFF) >>> 0; + this.refresh(); + break; + + case "PageDown": + this.address = (this.address + 16 * this.lines(true) & + 0xFFFFFFFF) >>> 0; + this.refresh(); + break; + + default: console.log(e.key); return; + } + + // Configure event + e.preventDefault(); + e.stopPropagation(); + } + + // Mouse wheel event handler + onwheel(e) { + let lineHeight = this.lineHeight(); + let sign = Math.sign(e.deltaY); + let mag = Math.abs (e.deltaY); + if (e.deltaMode == WheelEvent.DOM_DELTA_PIXEL) + mag = Math.max(1, Math.floor(mag / lineHeight)); + this.address = (this.address + sign * mag * 16 & 0xFFFFFFFF) >>> 0; + this.refresh(null, lineHeight); + } + + // Move to a new address positioned at a particular row of output + seek(address, index) { + this.address = (address - (index || 0) * 16 & 0xFFFFFFFF) >>> 0; + } + +}; + +// One row of output +MemoryWindow.Row = class Row { + + // Object constructor + constructor(client) { + + // Configure instance fields + this.bytes = new Array(16); + this.client = client; + this.spacers = new Array(2); + + // Address label + this.address = client.add(client.newLabel({ text: "\u00a0" })); + this.address.element.setAttribute("name", "address"); + + // Byte labels + for (let x = 0; x < 16; x++) { + if ((x & 7) == 0) { + let lbl = this.spacers[x / 8] = + client.add(client.newLabel({ text: "" })); + lbl.element.setAttribute("name", "byte"); + } + let lbl = this.bytes[x] = + client.add(client.newLabel({ text: "\u00a0" })); + lbl.element.setAttribute("name", "byte"); + } + + } + + + + ///////////////////////////// Package Methods ///////////////////////////// + + // Remove components from the memory window + remove() { + this.client.remove(this.address); + for (let bytel of this.bytes) + this.client.remove(bytel); + for (let spacer of this.spacers) + this.client.remove(spacer); + } + + // Update the output labels with emulation state content + update(address, bytes, offset) { + this.address.setText( + ("0000000" + address.toString(16).toUpperCase()).slice(-8)); + for (let x = 0; x < 16; x++, offset++) + this.bytes[x].setText( + ("0" + bytes[offset].toString(16).toUpperCase()).slice(-2)); + } + +}; diff --git a/core/bus.c b/core/bus.c new file mode 100644 index 0000000..7188dc9 --- /dev/null +++ b/core/bus.c @@ -0,0 +1,119 @@ +/* This file is included into vb.c and cannot be compiled on its own. */ +#ifdef VBAPI + + + +/**************************** Component Functions ****************************/ + +/* Read a value from a memory buffer */ +static int32_t busReadMemory(uint8_t *mem, int type) { + + /* Generic implementation */ + #ifdef VB_BIGENDIAN + switch (type) { + case VB_S8 : return (int8_t) *mem; + case VB_U8 : return *mem; + case VB_S16: return (int16_t ) (int8_t) mem[1] << 8 | mem[0]; + case VB_U16: return (uint16_t) mem[1] << 8 | mem[0]; + } + return (int32_t ) mem[3] << 24 | (uint32_t) mem[2] << 16 | + (uint32_t) mem[1] << 8 | mem[0]; + + /* Little-endian implementation */ + #else + switch (type) { + case VB_S8 : return *(int8_t *)mem; + case VB_U8 : return * mem; + case VB_S16: return *(int16_t *)mem; + case VB_U16: return *(uint16_t *)mem; + } + return *(int32_t *)mem; + #endif + +} + +/* Read a data unit from the bus */ +static int32_t busRead(VB *emu, uint32_t address, int type, int debug) { + + /* Force address alignment */ + address &= ~((uint32_t) TYPE_SIZES[type] - 1); + + /* Process by address range */ + switch (address >> 24 ^ 7) { + case 0 : return 0; /* VIP */ + case 1 : debug = debug; return 0; /* VSU */ + case 2 : return 0; /* Miscellaneous hardware */ + case 3 : return 0; /* Unmapped */ + case 4 : return 0; /* Game pak expansion */ + case 5 : return /* WRAM */ + busReadMemory(&emu->wram[address & 0xFFFF], type); + case 6 : return emu->cart.sram == NULL ? 0 : /* Game pak RAM */ + busReadMemory(&emu->cart.sram + [address & (emu->cart.sramSize - 1)], type); + default: return emu->cart.rom == NULL ? 0 : /* Game pak ROM */ + busReadMemory(&emu->cart.rom + [address & (emu->cart.romSize - 1)], type); + } + +} + +/* Write a data unit to a memory buffer */ +static void busWriteMemory(uint8_t *mem, int type, int32_t value) { + + /* Generic implementation */ + #ifdef VB_BIGENDIAN + switch (type) { + case VB_S32: + mem[3] = value >> 24; + mem[2] = value >> 16; + /* Fallthrough */ + case VB_S16: + case VB_U16: + mem[1] = value >> 8; + } + mem[0] = value; + + /* Little-endian implementation */ + #else + switch (type) { + case VB_S16: case VB_U16: *(uint16_t *)mem = value; return; + case VB_S8 : case VB_U8 : * mem = value; return; + } + *(int32_t *)mem = value; + #endif + +} + +/* Read a value from the bus */ +static void busWrite( + VB *emu, uint32_t address, int type, uint32_t value, int debug) { + + /* Force address alignment */ + address &= ~((uint32_t) TYPE_SIZES[type] - 1); + + /* Process by address range */ + switch (address >> 24 ^ 7) { + case 0 : return; /* VIP */ + case 1 : return; /* VSU */ + case 2 : return; /* Miscellaneous hardware */ + case 3 : return; /* Unmapped */ + case 4 : return; /* Game pak expansion */ + case 5 : /* WRAM */ + busWriteMemory(&emu->wram[address & 0xFFFF], type, value); + return; + case 6 : /* Cartridge RAM */ + if (emu->cart.sram != NULL) + busWriteMemory(&emu->cart.sram + [address & (emu->cart.sramSize - 1)], type, value); + return; + default: /* Cartridge ROM */ + if (debug && emu->cart.rom != NULL) + busWriteMemory(&emu->cart.rom + [address & (emu->cart.romSize - 1)], type, value); + } + +} + + + +#endif /* VBAPI */ diff --git a/core/vb.c b/core/vb.c new file mode 100644 index 0000000..fd6bb82 --- /dev/null +++ b/core/vb.c @@ -0,0 +1,196 @@ +#define VBAPI + +/* Header includes */ +#include + + + +/********************************* Constants *********************************/ + +/* Type sizes */ +static const uint8_t TYPE_SIZES[] = { 1, 1, 2, 2, 4 }; + + + +/********************************** Macros ***********************************/ + +/* Sign-extend a value of some number of bits to 32 bits */ +#define SignExtend(v,b) ( (v) | (((v) & 1 << b - 1) ? ~(int32_t)0 << b : 0) ) + + + +/*************************** Subsystem Components ****************************/ + +/* Component includes */ +#include "bus.c" + + + +/******************************* API Functions *******************************/ + +/* Retrieve the value of PC */ +uint32_t vbGetProgramCounter(VB *emu) { + return emu->cpu.pc; +} + +/* Retrieve the value of a program register */ +int32_t vbGetProgramRegister(VB *emu, int id) { + return id < 1 || id > 31 ? 0 : emu->cpu.program[id]; +} + +/* Retrieve the value of a system register */ +uint32_t vbGetSystemRegister(VB *emu, int id) { + switch (id) { + case VB_ADTRE: return emu->cpu.adtre; + case VB_CHCW : return emu->cpu.chcw.ice << 1; + case VB_EIPC : return emu->cpu.eipc; + case VB_EIPSW: return emu->cpu.eipsw; + case VB_FEPC : return emu->cpu.fepc; + case VB_FEPSW: return emu->cpu.fepsw; + case VB_PIR : return 0x00005346; + case VB_TKCW : return 0x000000E0; + case 29 : return emu->cpu.sr29; + case 31 : return emu->cpu.sr31; + case VB_ECR : return + (uint32_t) emu->cpu.ecr.fecc << 16 | emu->cpu.ecr.eicc; + case VB_PSW : return + (uint32_t) emu->cpu.psw.i << 16 | + (uint32_t) emu->cpu.psw.np << 15 | + (uint32_t) emu->cpu.psw.ep << 14 | + (uint32_t) emu->cpu.psw.ae << 13 | + (uint32_t) emu->cpu.psw.id << 12 | + (uint32_t) emu->cpu.psw.fro << 9 | + (uint32_t) emu->cpu.psw.fiv << 8 | + (uint32_t) emu->cpu.psw.fzd << 7 | + (uint32_t) emu->cpu.psw.fov << 6 | + (uint32_t) emu->cpu.psw.fud << 5 | + (uint32_t) emu->cpu.psw.fpr << 4 | + (uint32_t) emu->cpu.psw.cy << 3 | + (uint32_t) emu->cpu.psw.ov << 2 | + (uint32_t) emu->cpu.psw.s << 1 | + (uint32_t) emu->cpu.psw.z + ; + } + return 0; +} + +/* Prepare a simulation state instance for use */ +void vbInit(VB *emu) { + emu->cart.rom = NULL; + emu->cart.sram = NULL; +} + +/* Read a data unit from the bus */ +int32_t vbRead(VB *emu, uint32_t address, int type, int debug) { + return type < 0 || type >= (int) sizeof TYPE_SIZES ? 0 : + busRead(emu, address, type, debug); +} + +/* Simulate a hardware reset */ +void vbReset(VB *emu) { + uint32_t x; + + /* Initialize CPU registers */ + emu->cpu.pc = 0xFFFFFFF0; + vbSetSystemRegister(emu, VB_ECR, 0x0000FFF0); + vbSetSystemRegister(emu, VB_PSW, 0x00008000); + + /* Initialize extra CPU registers (the hardware does not do this) */ + emu->cpu.adtre = 0; + emu->cpu.eipc = 0; + emu->cpu.eipsw = 0; + emu->cpu.fepc = 0; + emu->cpu.fepsw = 0; + emu->cpu.sr29 = 0; + emu->cpu.sr31 = 0; + + /* Erase WRAM (the hardware does not do this) */ + for (x = 0; x < 0x10000; x++) + emu->wram[x] = 0; + +} + +/* Specify a new value for PC */ +uint32_t vbSetProgramCounter(VB *emu, uint32_t value) { + value &= 0xFFFFFFFE; + emu->cpu.pc = value; + /* Set stage to fecth=0 */ + return value; +} + +/* Specify a new value for a program register */ +int32_t vbSetProgramRegister(VB *emu, int id, int32_t value) { + return id < 1 || id > 31 ? 0 : (emu->cpu.program[id] = value); +} + +/* Supply a ROM buffer */ +int vbSetROM(VB *emu, void *rom, uint32_t size) { + + /* Check the buffer size */ + if (size < 1024 || size > 0x1000000 || ((size - 1) & size) != 0) + return 0; + + /* Configure the ROM buffer */ + emu->cart.rom = (uint8_t *) rom; + emu->cart.romSize = size; + return 1; +} + +/* Supply an SRAM buffer */ +int vbSetSRAM(VB *emu, void *sram, uint32_t size) { + + /* Check the buffer size */ + if (size == 0 || ((size - 1) & size) != 0) + return 0; + + /* Configure the SRAM buffer */ + emu->cart.sram = (uint8_t *) sram; + emu->cart.sramSize = size; + return 1; +} + +/* Specify a new value for a system register */ +uint32_t vbSetSystemRegister(VB *emu, int id, uint32_t value) { + switch (id) { + case VB_ADTRE: return emu->cpu.adtre = value & 0xFFFFFFFE; + case VB_EIPC : return emu->cpu.eipc = value & 0xFFFFFFFE; + case VB_EIPSW: return emu->cpu.eipsw = value & 0x000FF3FF; + case VB_FEPC : return emu->cpu.fepc = value & 0xFFFFFFFE; + case VB_FEPSW: return emu->cpu.fepsw = value & 0x000FF3FF; + case VB_PIR : return 0x00005346; + case VB_TKCW : return 0x000000E0; + case 29 : return emu->cpu.sr29 = value & 0x00000001; + case 31 : return emu->cpu.sr31 = value; + case VB_CHCW : + emu->cpu.chcw.ice = value >> 1 & 1; + return value & 0x00000002; + case VB_ECR : + emu->cpu.ecr.fecc = value >> 16; + emu->cpu.ecr.eicc = value; + return value; + case VB_PSW : + emu->cpu.psw.i = value >> 16 & 15; + emu->cpu.psw.np = value >> 15 & 1; + emu->cpu.psw.ep = value >> 14 & 1; + emu->cpu.psw.ae = value >> 13 & 1; + emu->cpu.psw.id = value >> 12 & 1; + emu->cpu.psw.fro = value >> 9 & 1; + emu->cpu.psw.fiv = value >> 8 & 1; + emu->cpu.psw.fzd = value >> 7 & 1; + emu->cpu.psw.fov = value >> 6 & 1; + emu->cpu.psw.fud = value >> 5 & 1; + emu->cpu.psw.fpr = value >> 4 & 1; + emu->cpu.psw.cy = value >> 3 & 1; + emu->cpu.psw.ov = value >> 2 & 1; + emu->cpu.psw.s = value >> 1 & 1; + emu->cpu.psw.z = value & 1; + return value & 0x000FF3FF; + } + return 0; +} + +/* Write a data unit to the bus */ +void vbWrite(VB *emu, uint32_t address, int type, int32_t value, int debug) { + if (type < 0 || type >= (int32_t) sizeof TYPE_SIZES) + busWrite(emu, address, type, value, debug); +} diff --git a/core/vb.h b/core/vb.h new file mode 100644 index 0000000..7858ecf --- /dev/null +++ b/core/vb.h @@ -0,0 +1,134 @@ +#ifndef __VB_H__ +#define __VB_H__ + +#ifdef __cplusplus +extern "C" { +#endif + +#ifndef VBAPI +#define VBAPI extern +#endif + +/* Header includes */ +#include +#include + + + +/********************************* Constants *********************************/ + +/* Value types */ +#define VB_S8 0 +#define VB_U8 1 +#define VB_S16 2 +#define VB_U16 3 +#define VB_S32 4 + +/* System register IDs */ +#define VB_ADTRE 25 +#define VB_CHCW 24 +#define VB_ECR 4 +#define VB_EIPC 0 +#define VB_EIPSW 1 +#define VB_FEPC 2 +#define VB_FEPSW 3 +#define VB_PIR 6 +#define VB_PSW 5 +#define VB_TKCW 7 + + + +/*********************************** Types ***********************************/ + +/* Simulation state */ +typedef struct VB VB; +struct VB { + + /* Game pak */ + struct { + uint8_t *rom; /* Active ROM buffer */ + uint32_t romSize; /* Size of ROM data */ + uint8_t *sram; /* Active SRAM buffer */ + uint32_t sramSize; /* Size of SRAM data */ + } cart; + + /* CPU */ + struct { + + /* System registers */ + uint32_t adtre; /* Address trap register for execution */ + uint32_t eipc; /* Exception/Interrupt PC */ + uint32_t eipsw; /* Exception/Interrupt PSW */ + uint32_t fepc; /* Fatal error PC */ + uint32_t fepsw; /* Fatal error PSW */ + uint32_t sr29; /* Unknown system register */ + uint32_t sr31; /* Unknown system register */ + + /* Cache control word */ + struct { + int8_t ice; /* Instruction cache enable */ + } chcw; + + /* Exception cause register */ + struct { + uint16_t eicc; /* Exception/interrupt cause code */ + uint16_t fecc; /* Fatal error cause code */ + } ecr; + + /* Program status word */ + struct { + int8_t ae; /* Address trap enable */ + int8_t cy; /* Carry */ + int8_t ep; /* Exception pending */ + int8_t fiv; /* Floating invalid */ + int8_t fov; /* Floating overflow */ + int8_t fpr; /* Floating precision */ + int8_t fro; /* Floating reserved operand */ + int8_t fud; /* Floating underflow */ + int8_t fzd; /* Floating zero divide */ + int8_t i; /* Interrupt level */ + int8_t id; /* Interrupt disable */ + int8_t np; /* NMI pending */ + int8_t ov; /* Overflow */ + int8_t s; /* Sign */ + int8_t z; /* Zero */ + } psw; + + /* Other registers */ + uint32_t pc; /* Program counter */ + int32_t program[32]; /* program registers */ + + /* Other fields */ + uint32_t clocks; /* Clocks until next action */ + uint8_t fatch; /* Index of fetch unit */ + uint8_t state; /* Operations state */ + } cpu; + + /* Other fields */ + uint8_t wram[0x10000]; /* Main memory */ +}; + + + +/**************************** Function Prototypes ****************************/ + +VBAPI uint32_t vbGetProgramCounter (VB *emu); +VBAPI int32_t vbGetProgramRegister (VB *emu, int id); +VBAPI uint32_t vbGetSystemRegister (VB *emu, int id); +VBAPI void vbInit (VB *emu); +VBAPI int32_t vbRead (VB *emu, uint32_t address, int type, int debug); +VBAPI void vbReset (VB *emu); +VBAPI uint32_t vbSetProgramCounter (VB *emu, uint32_t value); +VBAPI int32_t vbSetProgramRegister (VB *emu, int id, int32_t value); +VBAPI int vbSetROM (VB *emu, void *rom, uint32_t size); +VBAPI int vbSetSRAM (VB *emu, void *sram, uint32_t size); +VBAPI uint32_t vbSetSystemRegister (VB *emu, int id, uint32_t value); +VBAPI void vbWrite (VB *emu, uint32_t address, int type, int32_t value, int debug); + + + +#ifdef __cplusplus +} +#endif + +#endif /* __VB_H__ */ diff --git a/makefile b/makefile index 51f1122..9dd47a8 100644 --- a/makefile +++ b/makefile @@ -1,7 +1,7 @@ .PHONY: help help: @echo - @echo "Virtual Boy Emulator - August 26, 2021" + @echo "Virtual Boy Emulator - August 29, 2021" @echo @echo "Target build environment is any Debian with the following packages:" @echo " emscripten" @@ -9,16 +9,25 @@ help: @echo " openjdk-17-jdk" @echo @echo "Available make targets:" + @echo " build Perform all build commands" @echo " bundle Package the repository for HTML distribution" @echo " clean Remove output files" @echo " core Check the C core source for compiler warnings" @echo " wasm Build the WebAssembly core module" @echo +.PHONY: build +build: + @echo " Check C core library for style issues" + @make -s core + @echo " Build WebAssembly core module" + @make -s wasm + @echo " Bundle directory tree into HTML file" + @make -s bundle + .PHONY: bundle bundle: @java app/Bundle.java vbemu -# @gcc -c core/libvb.c -std=c90 -Wall -Wextra .PHONY: clean clean: @@ -26,12 +35,14 @@ clean: .PHONY: core core: - @echo "Check C core for style warnings" + @gcc core/vb.c -I core \ + -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 + @gcc core/vb.c -I core -D VB_BIGENDIAN \ + -fsyntax-only -Wall -Wextra -Werror -Wpedantic -std=c90 .PHONY: wasm wasm: - @echo "Build WASM core module" -# @emcc -o core.wasm wasm/*.c core/libvb.c -Icore \ -# --no-entry -O2 -flto -s WASM=1 -s EXPORTED_RUNTIME_METHODS=[] \ -# -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing + @emcc -o core.wasm wasm/wasm.c core/vb.c -Icore \ + --no-entry -O2 -flto -s WASM=1 -s EXPORTED_RUNTIME_METHODS=[] \ + -s ALLOW_MEMORY_GROWTH -s MAXIMUM_MEMORY=4GB -fno-strict-aliasing @rm -f *.wasm.tmp* diff --git a/wasm/wasm.c b/wasm/wasm.c new file mode 100644 index 0000000..a92e833 --- /dev/null +++ b/wasm/wasm.c @@ -0,0 +1,98 @@ +#include +#include +#include +#include +#include + + + +//////////////////////////////// Static Memory //////////////////////////////// + +static VB sims[2]; // Hardware simulations + + + +/////////////////////////////// Module Commands /////////////////////////////// + +// Proxy for free() +EMSCRIPTEN_KEEPALIVE void Free(void* ptr) { + free(ptr); +} + +// Proxy for malloc() +EMSCRIPTEN_KEEPALIVE void* Malloc(int size) { + return malloc(size); +} + +// Read multiple data units from the bus +EMSCRIPTEN_KEEPALIVE void ReadBuffer( + int sim, uint8_t *dest, uint32_t address, uint32_t size, int debug) { + for (; size > 0; address++, size--, dest++) + *dest = vbRead(&sims[sim], address, VB_U8, debug); +} + + + +//////////////////////////////// Core Commands //////////////////////////////// + +// Retrieve the value of PC +EMSCRIPTEN_KEEPALIVE uint32_t GetProgramCounter(int sim) { + return vbGetProgramCounter(&sims[sim]); +} + +// Retrieve the value of a program register +EMSCRIPTEN_KEEPALIVE int32_t GetProgramRegister(int sim, int id) { + return vbGetProgramRegister(&sims[sim], id); +} + +// Retrieve the value of a system register +EMSCRIPTEN_KEEPALIVE uint32_t GetSystemRegister(int sim, int id) { + return vbGetSystemRegister(&sims[sim], id); +} + +// Prepare simulation state instances for use +EMSCRIPTEN_KEEPALIVE void Init() { + vbInit(&sims[0]); + vbInit(&sims[1]); +} + +// Read a data unit from the bus +EMSCRIPTEN_KEEPALIVE int32_t Read(int sim,uint32_t address,int type,int debug){ + return vbRead(&sims[sim], address, type, debug); +} + +// Simulate a hardware reset +EMSCRIPTEN_KEEPALIVE void Reset(int sim) { + vbReset(&sims[sim]); +} + +// Specify a new value for PC +EMSCRIPTEN_KEEPALIVE uint32_t SetProgramCounter(int sim, uint32_t value) { + return vbSetProgramCounter(&sims[sim], value); +} + +// Specify a new value for a program register +EMSCRIPTEN_KEEPALIVE int32_t SetProgramRegister(int sim,int id,int32_t value) { + return vbSetProgramRegister(&sims[sim], id, value); +} + +// Supply a ROM buffer +EMSCRIPTEN_KEEPALIVE int SetROM(int sim, void *rom, uint32_t size) { + return vbSetROM(&sims[sim], rom, size); +} + +// Supply an SRAM buffer +EMSCRIPTEN_KEEPALIVE int SetSRAM(int sim, void *sram, uint32_t size) { + return vbSetSRAM(&sims[sim], sram, size); +} + +// Specify a new value for a system register +EMSCRIPTEN_KEEPALIVE uint32_t SetSystemRegister(int sim,int id,uint32_t value){ + return vbSetSystemRegister(&sims[sim], id, value); +} + +// Write a data unit to the bus +EMSCRIPTEN_KEEPALIVE void Write( + int sim, uint32_t address, int type, int32_t value, int debug) { + vbWrite(&sims[sim], address, type, value, debug); +}