🚀 First basic version

This commit is contained in:
Felix Rieseberg 2020-07-14 12:45:04 -07:00
commit 4950c7b153
25 changed files with 108425 additions and 0 deletions

89
.gitignore vendored Normal file
View File

@ -0,0 +1,89 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
.DS_Store
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# Webpack
.webpack/
# Electron-Forge
out/

58
package.json Normal file
View File

@ -0,0 +1,58 @@
{
"name": "macintosh",
"productName": "macintosh",
"version": "1.0.0",
"description": "Macintosh's System 7 in an Electron app. I'm sorry.",
"main": "src/index.js",
"scripts": {
"start": "electron-forge start",
"package": "electron-forge package",
"make": "electron-forge make",
"publish": "electron-forge publish",
"lint": "echo \"No linting configured\""
},
"keywords": [],
"author": {
"name": "Felix Rieseberg",
"email": "felix@felixrieseberg.com"
},
"license": "MIT",
"config": {
"forge": {
"packagerConfig": {},
"makers": [
{
"name": "@electron-forge/maker-squirrel",
"config": {
"name": "macintosh"
}
},
{
"name": "@electron-forge/maker-zip",
"platforms": [
"darwin"
]
},
{
"name": "@electron-forge/maker-deb",
"config": {}
},
{
"name": "@electron-forge/maker-rpm",
"config": {}
}
]
}
},
"dependencies": {
"electron-squirrel-startup": "^1.0.0"
},
"devDependencies": {
"@electron-forge/cli": "6.0.0-beta.52",
"@electron-forge/maker-deb": "6.0.0-beta.52",
"@electron-forge/maker-rpm": "6.0.0-beta.52",
"@electron-forge/maker-squirrel": "6.0.0-beta.52",
"@electron-forge/maker-zip": "6.0.0-beta.52",
"electron": "9.1.0"
}
}

View File

@ -0,0 +1,311 @@
var memAllocSet = new Set();
var memAllocSetPersistent = new Set();
function memAllocAdd(addr) {
if (memAllocSet.has(addr)) {
console.error(`unfreed memory alloc'd at ${addr}`);
}
memAllocSet.add(addr);
console.warn('malloc', addr);
memAllocSetPersistent.add(addr);
}
function memAllocRemove(addr) {
if (!memAllocSet.has(addr)) {
console.error(
`unalloc'd memory free'd at ${addr} (everallocd=${memAllocSetPersistent.has(
addr
)})`
);
}
console.warn('free', addr);
memAllocSet.delete(addr);
}
var pathGetFilenameRegex = /\/([^\/]+)$/;
function pathGetFilename(path) {
var matches = path.match(pathGetFilenameRegex);
if (matches && matches.length) {
return matches[1];
} else {
return path;
}
}
function addAutoloader(module) {
var loadDatafiles = function() {
module.autoloadFiles.forEach(function(filepath) {
module.FS_createPreloadedFile(
'/',
pathGetFilename(filepath),
filepath,
true,
true
);
});
};
if (module.autoloadFiles) {
module.preRun = module.preRun || [];
module.preRun.unshift(loadDatafiles);
}
return module;
}
function addCustomAsyncInit(module) {
if (module.asyncInit) {
module.preRun = module.preRun || [];
module.preRun.push(function waitForCustomAsyncInit() {
module.addRunDependency('__moduleAsyncInit');
module.asyncInit(module, function asyncInitCallback() {
module.removeRunDependency('__moduleAsyncInit');
});
});
}
}
var InputBufferAddresses = {
globalLockAddr: 0,
mouseMoveFlagAddr: 1,
mouseMoveXDeltaAddr: 2,
mouseMoveYDeltaAddr: 3,
mouseButtonStateAddr: 4,
keyEventFlagAddr: 5,
keyCodeAddr: 6,
keyStateAddr: 7,
};
var LockStates = {
READY_FOR_UI_THREAD: 0,
UI_THREAD_LOCK: 1,
READY_FOR_EMUL_THREAD: 2,
EMUL_THREAD_LOCK: 3,
};
var Module = null;
self.onmessage = function(msg) {
console.log('init worker');
startEmulator(Object.assign({}, msg.data, {singleThreadedEmscripten: true}));
};
function startEmulator(parentConfig) {
var screenBufferView = new Uint8Array(
parentConfig.screenBuffer,
0,
parentConfig.screenBufferSize
);
var videoModeBufferView = new Int32Array(
parentConfig.videoModeBuffer,
0,
parentConfig.videoModeBufferSize
);
var inputBufferView = new Int32Array(
parentConfig.inputBuffer,
0,
parentConfig.inputBufferSize
);
var nextAudioChunkIndex = 0;
var audioDataBufferView = new Uint8Array(
parentConfig.audioDataBuffer,
0,
parentConfig.audioDataBufferSize
);
function waitForTwoStateLock(bufferView, lockIndex) {
// Atomics.wait(
// bufferView,
// lockIndex,
// LockStates.UI_THREAD_LOCK
// );
// while (!tryToAcquireCyclicalLock(bufferView, lockIndex)) {
// // spin
// }
// if (!tryToAcquireCyclicalLock(bufferView, lockIndex)) {
// throw new Error('failed to acquire lock for index', lockIndex);
// }
//
//
if (Atomics.load(bufferView, lockIndex) === LockStates.UI_THREAD_LOCK) {
while (
Atomics.compareExchange(
bufferView,
lockIndex,
LockStates.UI_THREAD_LOCK,
LockStates.EMUL_THREAD_LOCK
) !== LockStates.UI_THREAD_LOCK
) {
// spin
// TODO use wait and wake
}
} else {
// already unlocked
}
}
function releaseTwoStateLock(bufferView, lockIndex) {
Atomics.store(bufferView, lockIndex, LockStates.UI_THREAD_LOCK); // unlock
}
function tryToAcquireCyclicalLock(bufferView, lockIndex) {
var res = Atomics.compareExchange(
bufferView,
lockIndex,
LockStates.READY_FOR_EMUL_THREAD,
LockStates.EMUL_THREAD_LOCK
);
if (res === LockStates.READY_FOR_EMUL_THREAD) {
return 1;
}
return 0;
}
function releaseCyclicalLock(bufferView, lockIndex) {
Atomics.store(bufferView, lockIndex, LockStates.READY_FOR_UI_THREAD); // unlock
}
function acquireInputLock() {
return tryToAcquireCyclicalLock(
inputBufferView,
InputBufferAddresses.globalLockAddr
);
}
function releaseInputLock() {
// reset
inputBufferView[InputBufferAddresses.mouseMoveFlagAddr] = 0;
inputBufferView[InputBufferAddresses.mouseMoveXDeltaAddr] = 0;
inputBufferView[InputBufferAddresses.mouseMoveYDeltaAddr] = 0;
inputBufferView[InputBufferAddresses.mouseButtonStateAddr] = 0;
inputBufferView[InputBufferAddresses.keyEventFlagAddr] = 0;
inputBufferView[InputBufferAddresses.keyCodeAddr] = 0;
inputBufferView[InputBufferAddresses.keyStateAddr] = 0;
releaseCyclicalLock(inputBufferView, InputBufferAddresses.globalLockAddr);
}
var AudioConfig = null;
var AudioBufferQueue = [];
Module = {
autoloadFiles: ['system7.img', 'DCImage.img', 'disk1.img', 'Quadra-650.rom', 'prefs'],
arguments: ['--config', 'prefs'],
canvas: null,
blit: function blit(bufPtr, width, height, depth, usingPalette) {
// console.time('await worker video lock');
// waitForTwoStateLock(videoModeBufferView, 9);
// console.timeEnd('await worker video lock');
videoModeBufferView[0] = width;
videoModeBufferView[1] = height;
videoModeBufferView[2] = depth;
videoModeBufferView[3] = usingPalette;
var length = width * height * (depth === 32 ? 4 : 1); // 32bpp or 8bpp
for (var i = 0; i < length; i++) {
screenBufferView[i] = Module.HEAPU8[bufPtr + i];
}
// releaseTwoStateLock(videoModeBufferView, 9);
},
openAudio: function openAudio(
sampleRate,
sampleSize,
channels,
framesPerBuffer
) {
AudioConfig = {
sampleRate: sampleRate,
sampleSize: sampleSize,
channels: channels,
framesPerBuffer: framesPerBuffer,
};
console.log(AudioConfig);
},
enqueueAudio: function enqueueAudio(bufPtr, nbytes, type) {
var newAudio = Module.HEAPU8.slice(bufPtr, bufPtr + nbytes);
// console.assert(
// nbytes == parentConfig.audioBlockBufferSize,
// `emulator wrote ${nbytes}, expected ${parentConfig.audioBlockBufferSize}`
// );
var writingChunkIndex = nextAudioChunkIndex;
var writingChunkAddr =
writingChunkIndex * parentConfig.audioBlockChunkSize;
if (audioDataBufferView[writingChunkAddr] === LockStates.UI_THREAD_LOCK) {
console.warn(
'worker tried to write audio data to UI-thread-locked chunk',
writingChunkIndex
);
return 0;
}
var nextNextChunkIndex = writingChunkIndex + 1;
if (
nextNextChunkIndex * parentConfig.audioBlockChunkSize >
audioDataBufferView.length - 1
) {
nextNextChunkIndex = 0;
}
// console.assert(nextNextChunkIndex != writingChunkIndex, `writingChunkIndex=${nextNextChunkIndex} == nextChunkIndex=${nextNextChunkIndex}`)
audioDataBufferView[writingChunkAddr + 1] = nextNextChunkIndex;
audioDataBufferView.set(newAudio, writingChunkAddr + 2);
audioDataBufferView[writingChunkAddr] = LockStates.UI_THREAD_LOCK;
nextAudioChunkIndex = nextNextChunkIndex;
return nbytes;
},
debugPointer: function debugPointer(ptr) {
console.log('debugPointer', ptr);
},
acquireInputLock: acquireInputLock,
InputBufferAddresses: InputBufferAddresses,
getInputValue: function getInputValue(addr) {
return inputBufferView[addr];
},
totalDependencies: 0,
monitorRunDependencies: function(left) {
this.totalDependencies = Math.max(this.totalDependencies, left);
if (left == 0) {
postMessage({type: 'emulator_ready'});
} else {
postMessage({
type: 'emulator_loading',
completion: (this.totalDependencies - left) / this.totalDependencies,
});
}
},
print: console.log.bind(console),
printErr: console.warn.bind(console),
releaseInputLock: releaseInputLock,
};
// inject extra behaviours
addAutoloader(Module);
addCustomAsyncInit(Module);
if (parentConfig.singleThreadedEmscripten) {
importScripts('BasiliskII.js');
}
}

104011
src/basilisk/BasiliskII.js Normal file

File diff suppressed because one or more lines are too long

Binary file not shown.

34
src/basilisk/prefs Normal file
View File

@ -0,0 +1,34 @@
disk system7.img
disk DCImage.img
extfs /
screen win/800/600
seriala
serialb
udptunnel false
udpport 6066
rom Quadra-650.rom
bootdrive 0
bootdriver 0
ramsize 268435456
frameskip 1
modelid 14
cpu 4
fpu true
nocdrom true
nosound false
noclipconversion false
nogui true
jit false
jitfpu false
jitdebug false
jitcachesize 8192
jitlazyflush true
jitinline true
keyboardtype 5
keycodes false
mousewheelmode 1
mousewheellines 3
dsp /dev/dsp
mixer /dev/mixer
ignoresegv false
idlewait false

58
src/index.js Normal file
View File

@ -0,0 +1,58 @@
const { app, BrowserWindow } = require('electron');
const path = require('path');
const { registerIpcHandlers } = require('./ipc');
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
if (require('electron-squirrel-startup')) { // eslint-disable-line global-require
app.quit();
}
const createWindow = () => {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 730,
useContentSize: true,
frame: false,
transparent: true,
resizable: false,
webPreferences: {
nodeIntegration: true
}
});
// and load the index.html of the app.
mainWindow.loadFile(path.join(__dirname, 'renderer/index.html'));
mainWindow.setMenu(null);
mainWindow.webContents.toggleDevTools();
};
// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.on('ready', () => {
registerIpcHandlers();
createWindow();
});
// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', () => {
if (process.platform !== 'darwin') {
app.quit();
}
});
app.on('activate', () => {
// On OS X it's common to re-create a window in the app when the
// dock icon is clicked and there are no other windows open.
if (BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});
// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and import them here.

9
src/ipc.js Normal file
View File

@ -0,0 +1,9 @@
const { ipcMain, app } = require('electron');
function registerIpcHandlers() {
ipcMain.handle('quit', () => app.quit())
}
module.exports = {
registerIpcHandlers
}

46
src/renderer/atomics.js Normal file
View File

@ -0,0 +1,46 @@
const LockStates = {
READY_FOR_UI_THREAD: 0,
UI_THREAD_LOCK: 1,
READY_FOR_EMUL_THREAD: 2,
EMUL_THREAD_LOCK: 3,
};
function acquireTwoStateLock(bufferView, lockIndex) {
const res = Atomics.compareExchange(
bufferView,
lockIndex,
LockStates.EMUL_THREAD_LOCK,
LockStates.UI_THREAD_LOCK
);
return res === LockStates.EMUL_THREAD_LOCK;
}
function releaseTwoStateLock(bufferView, lockIndex) {
Atomics.store(bufferView, lockIndex, LockStates.EMUL_THREAD_LOCK); // unlock
Atomics.notify(bufferView, lockIndex);
}
function acquireLock(bufferView, lockIndex) {
const res = Atomics.compareExchange(
bufferView,
lockIndex,
LockStates.READY_FOR_UI_THREAD,
LockStates.UI_THREAD_LOCK
);
return res === LockStates.READY_FOR_UI_THREAD;
}
function releaseLock(bufferView, lockIndex) {
Atomics.store(bufferView, lockIndex, LockStates.READY_FOR_EMUL_THREAD); // unlock
Atomics.notify(bufferView, lockIndex);
}
module.exports = {
acquireTwoStateLock,
releaseTwoStateLock,
acquireLock,
releaseLock,
LockStates
}

238
src/renderer/audio.js Normal file
View File

@ -0,0 +1,238 @@
const { LockStates } = require('./atomics');
var audio = {
channels: 1,
bytesPerSample: 2,
samples: 4096,
// freq: 11025,
freq: 22050,
// freq: 44100,
format: 0x8010,
paused: false,
timer: null,
silence: 0,
maxBuffersInSharedMemory: 5,
nextPlayTime: 0,
};
var audioTotalSamples = audio.samples * audio.channels;
audio.bytesPerSample =
audio.format == 0x0008 /*AUDIO_U8*/ || audio.format == 0x8008 /*AUDIO_S8*/
? 1
: 2;
audio.bufferSize = audioTotalSamples * audio.bytesPerSample;
audio.bufferDurationSecs =
audio.bufferSize / audio.bytesPerSample / audio.channels / audio.freq; // Duration of a single queued buffer in seconds.
audio.bufferingDelay = 50 / 1000; // Audio samples are played with a constant delay of this many seconds to account for browser and jitter.
// To account for jittering in frametimes, always have multiple audio buffers queued up for the audio output device.
// This helps that we won't starve that easily if a frame takes long to complete.
audio.numSimultaneouslyQueuedBuffers = 5;
audio.nextChunkIndex = 0;
var AUDIO_CONFIG_BUFFER_SIZE = 10;
var audioConfigBuffer = new SharedArrayBuffer(AUDIO_CONFIG_BUFFER_SIZE);
var audioConfigBufferView = new Uint8Array(audioConfigBuffer);
var audioBlockChunkSize = audio.bufferSize + 2;
var AUDIO_DATA_BUFFER_SIZE =
audioBlockChunkSize * audio.maxBuffersInSharedMemory;
var audioDataBuffer = new SharedArrayBuffer(AUDIO_DATA_BUFFER_SIZE);
var audioDataBufferView = new Uint8Array(audioDataBuffer);
var audioContext = new AudioContext();
// document.querySelector('#enableAudio').addEventListener('click', function() {
// audioContext.resume().then(() => {
// document.querySelector('#enableAudio').remove();
// });
// });
var gainNode = audioContext.createGain();
gainNode.gain.value = 1;
gainNode.connect(audioContext.destination);
function openAudio() {
audio.pushAudio = function pushAudio(
blockBuffer, // u8 typed array
sizeBytes // probably (frames per block=4096) * (bytes per sample=2) * (n channels=1)
) {
if (audio.paused) return;
var sizeSamples = sizeBytes / audio.bytesPerSample; // How many samples fit in the callback buffer?
var sizeSamplesPerChannel = sizeSamples / audio.channels; // How many samples per a single channel fit in the cb buffer?
if (sizeSamplesPerChannel != audio.samples) {
throw 'Received mismatching audio buffer size!';
}
// Allocate new sound buffer to be played.
var source = audioContext.createBufferSource();
var soundBuffer = audioContext.createBuffer(
audio.channels,
sizeSamplesPerChannel,
audio.freq
);
// source.connect(audioContext.destination);
source.connect(gainNode);
audio.fillWebAudioBufferFromChunk(
blockBuffer,
sizeSamplesPerChannel,
soundBuffer
);
// Workaround https://bugzilla.mozilla.org/show_bug.cgi?id=883675 by setting the buffer only after filling. The order is important here!
source.buffer = soundBuffer;
// Schedule the generated sample buffer to be played out at the correct time right after the previously scheduled
// sample buffer has finished.
var curtime = audioContext.currentTime;
// assertion
if (curtime > audio.nextPlayTime && audio.nextPlayTime != 0) {
console.log(
'warning: Audio callback had starved sending audio by ' +
(curtime - audio.nextPlayTime) +
' seconds.'
);
}
// Don't ever start buffer playbacks earlier from current time than a given constant 'audio.bufferingDelay', since a browser
// may not be able to mix that audio clip in immediately, and there may be subsequent jitter that might cause the stream to starve.
var playtime = Math.max(curtime + audio.bufferingDelay, audio.nextPlayTime);
source.start(playtime);
// console.log(`queuing audio for ${playtime}`)
audio.nextPlayTime = playtime + audio.bufferDurationSecs;
};
var getBlockBufferLastWarningTime = 0;
var getBlockBufferWarningCount = 0;
audio.getBlockBuffer = function getBlockBuffer() {
// audio chunk layout
// 0: lock state
// 1: pointer to next chunk
// 2->buffersize+2: audio buffer
var curChunkIndex = audio.nextChunkIndex;
var curChunkAddr = curChunkIndex * audioBlockChunkSize;
if (audioDataBufferView[curChunkAddr] !== LockStates.UI_THREAD_LOCK) {
getBlockBufferWarningCount++;
if (
audio.gotFirstBlock &&
Date.now() - getBlockBufferLastWarningTime > 5000
) {
console.warn(
`UI thread tried to read audio data from worker-locked chunk ${getBlockBufferWarningCount} times`
);
console.log('curChunkIndex', curChunkIndex);
// debugger
getBlockBufferLastWarningTime = Date.now();
getBlockBufferWarningCount = 0;
}
return null;
}
audio.gotFirstBlock = true;
// debugger
var blockBuffer = audioDataBufferView.slice(
curChunkAddr + 2,
curChunkAddr + 2 + audio.bufferSize
);
audio.nextChunkIndex = audioDataBufferView[curChunkAddr + 1];
// console.assert(audio.nextChunkIndex != curChunkIndex, `curChunkIndex=${curChunkIndex} == nextChunkIndex=${audio.nextChunkIndex}`)
audioDataBufferView[curChunkAddr] = LockStates.EMUL_THREAD_LOCK;
// debugger
// console.log(`got buffer=${curChunkIndex}, next=${audio.nextChunkIndex}`)
return blockBuffer;
};
audio.fillWebAudioBufferFromChunk = function fillWebAudioBufferFromChunk(
blockBuffer, // u8 typed array
blockSize, // probably 4096
dstAudioBuffer
) {
for (var c = 0; c < audio.channels; ++c) {
var channelData = dstAudioBuffer.getChannelData(c);
if (channelData.length != blockSize) {
throw 'Web Audio output buffer length mismatch! Destination size: ' +
channelData.length +
' samples vs expected ' +
blockSize +
' samples!';
}
var blockBufferI16 = new Int16Array(blockBuffer.buffer);
for (var j = 0; j < blockSize; ++j) {
channelData[j] = blockBufferI16[j] / 0x8000; // convert i16 to f32 in range -1 to +1
}
}
};
// Pulls and queues new audio data if appropriate. This function gets "over-called" in both requestAnimationFrames and
// setTimeouts to ensure that we get the finest granularity possible and as many chances from the browser to fill
// new audio data. This is because setTimeouts alone have very poor granularity for audio streaming purposes, but also
// the application might not be using emscripten_set_main_loop to drive the main loop, so we cannot rely on that alone.
audio.queueNewAudioData = function queueNewAudioData() {
var i = 0;
for (; i < audio.numSimultaneouslyQueuedBuffers; ++i) {
// Only queue new data if we don't have enough audio data already in queue. Otherwise skip this time slot
// and wait to queue more in the next time the callback is run.
var secsUntilNextPlayStart =
audio.nextPlayTime - audioContext.currentTime;
if (
secsUntilNextPlayStart >=
audio.bufferingDelay +
audio.bufferDurationSecs * audio.numSimultaneouslyQueuedBuffers
)
return;
var blockBuffer = audio.getBlockBuffer();
if (!blockBuffer) {
return;
}
// And queue it to be played after the currently playing audio stream.
audio.pushAudio(blockBuffer, audio.bufferSize);
}
// console.log(`queued ${i} buffers of audio`);
};
// Create a callback function that will be routinely called to ask more audio data from the user application.
audio.caller = function audioCaller() {
--audio.numAudioTimersPending;
audio.queueNewAudioData();
// Queue this callback function to be called again later to pull more audio data.
var secsUntilNextPlayStart = audio.nextPlayTime - audioContext.currentTime;
// Queue the next audio frame push to be performed half-way when the previously queued buffer has finished playing.
var preemptBufferFeedSecs = audio.bufferDurationSecs / 2.0;
if (audio.numAudioTimersPending < audio.numSimultaneouslyQueuedBuffers) {
++audio.numAudioTimersPending;
audio.timer = setTimeout(
audio.caller,
Math.max(0.0, 1000.0 * (secsUntilNextPlayStart - preemptBufferFeedSecs))
);
// If we are risking starving, immediately queue an extra buffer.
if (audio.numAudioTimersPending < audio.numSimultaneouslyQueuedBuffers) {
++audio.numAudioTimersPending;
setTimeout(audio.caller, 1.0);
}
}
};
audio.numAudioTimersPending = 1;
audio.timer = setTimeout(audio.caller, 1);
}
module.exports = {
audio,
audioDataBuffer,
audioContext,
audioBlockChunkSize,
AUDIO_DATA_BUFFER_SIZE,
openAudio
}

9
src/renderer/controls.js vendored Normal file
View File

@ -0,0 +1,9 @@
const { quit } = require("./ipc");
function registerControls() {
document.querySelector('#close').addEventListener('click', () => {
quit();
});
}
registerControls();

19
src/renderer/emulator.js Normal file
View File

@ -0,0 +1,19 @@
const { drawScreen } = require('./screen');
const { openAudio } = require('./audio');
const { tryToSendInput } = require('./input');
const { registerWorker } = require('./worker');
registerWorker();
function asyncLoop() {
drawScreen();
tryToSendInput();
requestAnimationFrame(asyncLoop);
}
function start() {
openAudio();
asyncLoop();
}
start();

BIN
src/renderer/garamond.ttf Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 863 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 420 B

51
src/renderer/index.css Normal file
View File

@ -0,0 +1,51 @@
@font-face {
font-family: Garamond;
src: url(garamond.ttf);
}
body {
font-family: Garamond, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0 auto;
padding: 0;
}
canvas {
cursor: none;
}
.controls {
text-align: center;
margin-top: 5px;
background-color: #000;
width: 800px;
-webkit-app-region: drag;
height: 24px;
color: #020202;
background-image: url(images/tile_bg_end.png), url(images/tile_bg_end_right.png), url(images/tile_bg.png);
background-position: left, right, center;
background-repeat: no-repeat, no-repeat, repeat-x;
background-size: contain;
font-size: 12px;
}
.controls .clear {
display: inline-block;
height: calc(100% - 3px);
background-image: url(images/tile_clear_bg.png);
background-repeat: repeat-x;
background-size: contain;
padding-top: 3px;
padding-left: 10px;
padding-right: 10px;
}
.controls a {
font-weight: bold;
text-decoration: none;
-webkit-app-region: none;
color: #000;
}
.controls a:hover {
text-decoration: underline;
}

18
src/renderer/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Hello World!</title>
<link rel="stylesheet" href="index.css">
</head>
<body>
<canvas id="canvas" width="800" height="600" oncontextmenu="event.preventDefault()"></canvas>
<div class="controls">
<div class="clear">
<a id="close" href="#">Quit</a> - <a id="credits" href="#">Credits</a>
</div>
</div>
<script src="./controls.js"></script>
<script src="./emulator.js"></script>
</body>
</html>

107
src/renderer/input.js Normal file
View File

@ -0,0 +1,107 @@
const { acquireLock, releaseLock } = require('./atomics');
const INPUT_BUFFER_SIZE = 100;
const inputBuffer = new SharedArrayBuffer(INPUT_BUFFER_SIZE * 4);
const inputBufferView = new Int32Array(inputBuffer);
let inputQueue = [];
const InputBufferAddresses = {
globalLockAddr: 0,
mouseMoveFlagAddr: 1,
mouseMoveXDeltaAddr: 2,
mouseMoveYDeltaAddr: 3,
mouseButtonStateAddr: 4,
keyEventFlagAddr: 5,
keyCodeAddr: 6,
keyStateAddr: 7,
};
function releaseInputLock() {
releaseLock(inputBufferView, InputBufferAddresses.globalLockAddr);
}
function tryToSendInput() {
if (!acquireLock(inputBufferView, InputBufferAddresses.globalLockAddr)) {
return;
}
var hasMouseMove = false;
var mouseMoveX = 0;
var mouseMoveY = 0;
var mouseButtonState = -1;
var hasKeyEvent = false;
var keyCode = -1;
var keyState = -1;
// currently only one key event can be sent per sync
// TODO: better key handling code
var remainingKeyEvents = [];
for (var i = 0; i < inputQueue.length; i++) {
var inputEvent = inputQueue[i];
switch (inputEvent.type) {
case 'mousemove':
hasMouseMove = true;
mouseMoveX += inputEvent.dx;
mouseMoveY += inputEvent.dy;
break;
case 'mousedown':
case 'mouseup':
mouseButtonState = inputEvent.type === 'mousedown' ? 1 : 0;
break;
case 'keydown':
case 'keyup':
if (hasKeyEvent) {
remainingKeyEvents.push(inputEvent);
break;
}
hasKeyEvent = true;
keyState = inputEvent.type === 'keydown' ? 1 : 0;
keyCode = inputEvent.keyCode;
break;
}
}
if (hasMouseMove) {
inputBufferView[InputBufferAddresses.mouseMoveFlagAddr] = 1;
inputBufferView[InputBufferAddresses.mouseMoveXDeltaAddr] = mouseMoveX;
inputBufferView[InputBufferAddresses.mouseMoveYDeltaAddr] = mouseMoveY;
}
inputBufferView[InputBufferAddresses.mouseButtonStateAddr] = mouseButtonState;
if (hasKeyEvent) {
inputBufferView[InputBufferAddresses.keyEventFlagAddr] = 1;
inputBufferView[InputBufferAddresses.keyCodeAddr] = keyCode;
inputBufferView[InputBufferAddresses.keyStateAddr] = keyState;
}
releaseInputLock();
inputQueue = remainingKeyEvents;
}
canvas.addEventListener('mousemove', function (event) {
inputQueue.push({ type: 'mousemove', dx: event.offsetX, dy: event.offsetY });
});
canvas.addEventListener('mousedown', function (event) {
inputQueue.push({ type: 'mousedown' });
});
canvas.addEventListener('mouseup', function (event) {
inputQueue.push({ type: 'mouseup' });
});
window.addEventListener('keydown', function (event) {
inputQueue.push({ type: 'keydown', keyCode: event.keyCode });
});
window.addEventListener('keyup', function (event) {
inputQueue.push({ type: 'keyup', keyCode: event.keyCode });
});
module.exports = {
INPUT_BUFFER_SIZE,
inputBuffer,
inputBufferView,
InputBufferAddresses,
inputQueue,
tryToSendInput
}

7
src/renderer/ipc.js Normal file
View File

@ -0,0 +1,7 @@
const { ipcRenderer } = require('electron');
module.exports = {
quit() {
ipcRenderer.invoke('quit');
}
}

52
src/renderer/screen.js Normal file
View File

@ -0,0 +1,52 @@
const { videoModeBufferView } = require('./video');
const { audioContext } = require('./audio');
const SCREEN_WIDTH = 800;
const SCREEN_HEIGHT = 600;
const SCREEN_BUFFER_SIZE = SCREEN_WIDTH * SCREEN_HEIGHT * 4; // 32bpp;
const screenBuffer = new SharedArrayBuffer(SCREEN_BUFFER_SIZE);
const screenBufferView = new Uint8Array(screenBuffer);
canvas.width = SCREEN_WIDTH;
canvas.height = SCREEN_HEIGHT;
const canvasCtx = canvas.getContext('2d');
const imageData = canvasCtx.createImageData(SCREEN_WIDTH, SCREEN_HEIGHT);
function drawScreen() {
const pixelsRGBA = imageData.data;
const numPixels = SCREEN_WIDTH * SCREEN_HEIGHT;
const expandedFromPalettedMode = videoModeBufferView[3];
const start = audioContext.currentTime;
if (expandedFromPalettedMode) {
for (var i = 0; i < numPixels; i++) {
// palette
pixelsRGBA[i * 4 + 0] = screenBufferView[i * 4 + 0];
pixelsRGBA[i * 4 + 1] = screenBufferView[i * 4 + 1];
pixelsRGBA[i * 4 + 2] = screenBufferView[i * 4 + 2];
pixelsRGBA[i * 4 + 3] = 255; // full opacity
}
} else {
for (var i = 0; i < numPixels; i++) {
// ARGB
pixelsRGBA[i * 4 + 0] = screenBufferView[i * 4 + 1];
pixelsRGBA[i * 4 + 1] = screenBufferView[i * 4 + 2];
pixelsRGBA[i * 4 + 2] = screenBufferView[i * 4 + 3];
pixelsRGBA[i * 4 + 3] = 255; // full opacity
}
}
canvasCtx.putImageData(imageData, 0, 0);
}
module.exports = {
screenBuffer,
screenBufferView,
SCREEN_BUFFER_SIZE,
drawScreen,
SCREEN_WIDTH,
SCREEN_HEIGHT
}

13
src/renderer/video.js Normal file
View File

@ -0,0 +1,13 @@
const { releaseTwoStateLock } = require('./atomics')
const VIDEO_MODE_BUFFER_SIZE = 10;
const videoModeBuffer = new SharedArrayBuffer(VIDEO_MODE_BUFFER_SIZE * 4);
const videoModeBufferView = new Int32Array(videoModeBuffer);
releaseTwoStateLock(videoModeBufferView, 9);
module.exports = {
VIDEO_MODE_BUFFER_SIZE,
videoModeBuffer,
videoModeBufferView
}

50
src/renderer/worker.js Normal file
View File

@ -0,0 +1,50 @@
const { inputBuffer, INPUT_BUFFER_SIZE } = require('./input')
const { videoModeBuffer, VIDEO_MODE_BUFFER_SIZE } = require('./video')
const { screenBuffer, SCREEN_BUFFER_SIZE, SCREEN_WIDTH, SCREEN_HEIGHT } = require('./screen')
const { audio, audioDataBuffer, audioBlockChunkSize, AUDIO_DATA_BUFFER_SIZE } = require('./audio')
function registerWorker() {
var workerConfig = {
inputBuffer: inputBuffer,
inputBufferSize: INPUT_BUFFER_SIZE,
screenBuffer: screenBuffer,
screenBufferSize: SCREEN_BUFFER_SIZE,
videoModeBuffer: videoModeBuffer,
videoModeBufferSize: VIDEO_MODE_BUFFER_SIZE,
audioDataBuffer: audioDataBuffer,
audioDataBufferSize: AUDIO_DATA_BUFFER_SIZE,
audioBlockBufferSize: audio.bufferSize,
audioBlockChunkSize: audioBlockChunkSize,
SCREEN_WIDTH: SCREEN_WIDTH,
SCREEN_HEIGHT: SCREEN_HEIGHT,
};
var worker = new Worker('../basilisk/BasiliskII-worker-boot.js');
worker.postMessage(workerConfig);
worker.onmessage = function(e) {
if (
e.data.type === 'emulator_ready' ||
e.data.type === 'emulator_loading'
) {
// document.body.className =
// e.data.type === 'emulator_ready' ? '' : 'loading';
// const progressElement = document.getElementById('progress');
// if (progressElement && e.data.type === 'emulator_loading') {
// progressElement.value = Math.max(10, e.data.completion * 100);
// progressElement.max = 100;
// progressElement.hidden = false;
// } else {
// progressElement.value = null;
// progressElement.max = null;
// progressElement.hidden = true;
// }
}
};
}
module.exports = {
registerWorker
}

3245
yarn.lock Normal file

File diff suppressed because it is too large Load Diff