Adding Memory window
This commit is contained in:
parent
f753f9f59b
commit
b2adf1f9dc
110
app/App.js
110
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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
|
@ -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();
|
|
@ -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();
|
||||
|
|
|
@ -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."
|
||||
|
@ -13,7 +14,10 @@
|
|||
},
|
||||
menu: {
|
||||
_ : "Main application menu",
|
||||
file: {
|
||||
debug: {
|
||||
_: "Debug",
|
||||
},
|
||||
file : {
|
||||
_ : "File",
|
||||
loadROM: "Load ROM..."
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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"] {
|
||||
line-height: 1em;
|
||||
margin : 1px;
|
||||
padding: 3px;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
};
|
|
@ -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();
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -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
|
||||
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" );
|
||||
}
|
||||
|
||||
};
|
||||
|
|
|
@ -9,10 +9,15 @@ Toolkit.Window = class Window extends Toolkit.Panel {
|
|||
options = options || {};
|
||||
|
||||
// Configure instance fields
|
||||
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
|
||||
|
@ -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) {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
};
|
|
@ -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 */
|
|
@ -0,0 +1,196 @@
|
|||
#define VBAPI
|
||||
|
||||
/* Header includes */
|
||||
#include <vb.h>
|
||||
|
||||
|
||||
|
||||
/********************************* 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);
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
#ifndef __VB_H__
|
||||
#define __VB_H__
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C" {
|
||||
#endif
|
||||
|
||||
#ifndef VBAPI
|
||||
#define VBAPI extern
|
||||
#endif
|
||||
|
||||
/* Header includes */
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
|
||||
|
||||
/********************************* 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__ */
|
25
makefile
25
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*
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <emscripten/emscripten.h>
|
||||
#include <vb.h>
|
||||
|
||||
|
||||
|
||||
//////////////////////////////// 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);
|
||||
}
|
Loading…
Reference in New Issue