Establishing web architecture

This commit is contained in:
Guy Perfect 2021-08-22 17:32:18 -05:00
parent 4b3852a1d2
commit 0927a42107
5 changed files with 540 additions and 0 deletions

5
app/App.js Normal file
View File

@ -0,0 +1,5 @@
"use strict";
globalThis.App = class App {
};

214
app/Bundle.java Normal file
View File

@ -0,0 +1,214 @@
import java.awt.image.*;
import java.io.*;
import java.nio.charset.*;
import java.util.*;
import javax.imageio.*;
public class Bundle {
// File loaded from disk
static class File2 implements Comparable<File2> {
// Instance fields
byte[] data; // Contents
String filename; // Full path relative to root
// Comparator
public int compareTo(File2 o) {
if (filename.equals("app/_boot.js"))
return -1;
if (o.filename.equals("app/_boot.js"))
return 1;
return filename.compareTo(o.filename);
}
}
// Load all files in directory tree into memory
static HashMap<String, File2> readFiles(String bundleTitle) {
var dirs = new ArrayDeque<File>();
var root = new File(".");
var subs = new ArrayDeque<String>();
var files = new HashMap<String, File2>();
// Process all subdirectories
dirs.add(root);
while (!dirs.isEmpty()) {
var dir = dirs.remove();
// Add all subdirectories
for (var sub : dir.listFiles(f->f.isDirectory())) {
// Exclusions
if (dir == root && sub.getName().equals(".git"))
continue;
// Add the directory for bundling
dirs.add(sub);
}
// Add all files
for (var file : dir.listFiles(f->f.isFile())) {
var file2 = new File2();
// Read the file into memory
try {
var stream = new FileInputStream(file);
file2.data = stream.readAllBytes();
stream.close();
} catch (Exception e) {
throw new RuntimeException(e.getMessage());
}
// Determine the file's full pathname
subs.clear();
subs.addFirst(file.getName());
for (;;) {
file = file.getParentFile();
if (file.equals(root))
break;
subs.addFirst(file.getName());
}
file2.filename = String.join("/", subs);
// Exclusions
if (
file2.filename.startsWith(".git" ) ||
file2.filename.startsWith(bundleTitle + "_") &&
file2.filename.endsWith (".html" )
) continue;
// Add the file to the output
files.put(file2.filename, file2);
}
}
return files;
}
// Prepend manifest object to _boot.js
static void manifest(HashMap<String, File2> files, String bundleName) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build a file manifest
var manifest = new StringBuilder();
manifest.append("\"use strict\";\nlet manifest=[");
for (var file : values) {
manifest.append("{" +
"name:\"" + file.filename + "\"," +
"size:" + file.data.length +
"},");
}
manifest.append("];\nlet bundleName=\"" + bundleName + "\";\n");
// Prepend the manifest to _boot.js
var boot = files.get("app/_boot.js");
boot.data = (
manifest.toString() +
new String(boot.data, StandardCharsets.UTF_8) +
"\u0000"
).getBytes(StandardCharsets.UTF_8);
}
// Construct bundled blob
static byte[] blob(HashMap<String, File2> files) {
// Produce a sorted list of files
var values = files.values().toArray(new File2[files.size()]);
Arrays.sort(values);
// Build the blob
var blob = new ByteArrayOutputStream();
for (var file : values) try {
blob.write(file.data);
} catch (Exception e) { }
return blob.toByteArray();
}
// Encode bundled blob as a .png
static byte[] png(byte[] blob) {
// Calculate the dimensions of the image
int width = (int) Math.ceil(Math.sqrt(blob.length));
int height = (int) Math.ceil((double) blob.length / width);
// Prepare the pixel data
var pixels = new int[width * height];
for (int x = 0; x < blob.length; x++) {
int l = blob[x] & 0xFF;
pixels[x] = 0xFF000000 | l << 16 | l << 8 | l;
}
// Produce a BufferedImage containing the pixels
var img = new BufferedImage(width, height,
BufferedImage.TYPE_BYTE_GRAY);
img.getRaster().setPixels(0, 0, width, height, pixels);
// Encode the image as a PNG byte array
var png = new ByteArrayOutputStream();
try { ImageIO.write(img, "png", png); }
catch (Exception e) { }
return png.toByteArray();
}
// Embed bundle .png into template.html as a data URL
static void template(byte[] png, String bundleName) {
// Encode the PNG as a data URL
String url = "data:image/png;base64," +
Base64.getMimeEncoder().encodeToString(png)
.replaceAll("\\r\\n", "");
try {
// Read template.html into memory
var inStream = new FileInputStream("app/template.html");
String template =
new String(inStream.readAllBytes(), StandardCharsets.UTF_8)
.replace("src=\"\"", "src=\"" + url + "\"")
;
inStream.close();
// Write the output HTML file
var outStream = new FileOutputStream(bundleName + ".html");
outStream.write(template.getBytes(StandardCharsets.UTF_8));
outStream.close();
} catch (Exception e) { throw new RuntimeException(e.getMessage()); }
}
// Determine the filename of the bundle
static String bundleName(String name) {
var calendar = Calendar.getInstance();
return String.format("%s_%04d%02d%02d",
name,
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH) + 1,
calendar.get(Calendar.DAY_OF_MONTH)
);
}
// Program entry point
public static void main(String[] args) {
// Error checking
if (args.length != 1) {
System.err.println("Usage: Bundle <name>");
return;
}
// Program tasks
String bundleName = bundleName(args[0]);
var files = readFiles(args[0]);
manifest(files, bundleName);
var blob = blob(files);
var png = png(blob);
template(png, bundleName);
}
}

274
app/_boot.js Normal file
View File

@ -0,0 +1,274 @@
// Produce an async function from a source string
if (!globalThis.AsyncFunction)
globalThis.AsyncFunction =
Object.getPrototypeOf(async function(){}).constructor;
// Read scripts from files on disk to aid with debugging
let debug = location.hash == "#debug";
///////////////////////////////////////////////////////////////////////////////
// Bundle //
///////////////////////////////////////////////////////////////////////////////
// Resource asset manager
globalThis.Bundle = class BundledFile {
///////////////////////////// Static Methods //////////////////////////////
// Adds a bundled file from loaded file data
static add(name, data) {
return Bundle.files[name] = new Bundle(name, data);
}
// Retrieve the file given its filename
static get(name) {
return Bundle.files[name];
}
// Run a file as a JavaScript source file
static async run(name) {
await Bundle.files[name].run();
}
// Resolve a URL for a script source file
static script(name) {
return debug ? name : Bundle.files[name].toDataURL();
}
///////////////////////// Initialization Methods //////////////////////////
// Object constructor
constructor(name, data) {
// Configure instance fields
this.data = data;
this.name = name;
// Detect the MIME type
this.mime =
name.endsWith(".css" ) ? "text/css;charset=UTF-8" :
name.endsWith(".frag") ? "text/plain;charset=UTF-8" :
name.endsWith(".js" ) ? "text/javascript;charset=UTF-8" :
name.endsWith(".png" ) ? "image/png" :
name.endsWith(".svg" ) ? "image/svg+xml;charset=UTF-8" :
name.endsWith(".txt" ) ? "text/plain;charset=UTF-8" :
name.endsWith(".vert") ? "text/plain;charset=UTF-8" :
name.endsWith(".wasm") ? "application/wasm" :
"application/octet-stream"
;
}
///////////////////////////// Public Methods //////////////////////////////
// Execute the file as a JavaScript source file
async run() {
// Not running in debug mode
if (!debug) {
await new AsyncFunction(this.toString())();
return;
}
// Running in debug mode
return new Promise((resolve,reject)=>{
let script = document.createElement("script");
document.head.appendChild(script);
script.addEventListener("load", ()=>resolve());
script.src = this.name;
});
}
// Produce a blob from the file data
toBlob() {
return new Blob(this.data, { type: this.mime });
}
// Produce a blob URL for the file data
toBlobURL() {
return URL.createObjectURL(this.toBlob());
}
// Encode the file data as a data URL
toDataURL() {
return "data:" + this.mime + ";base64," + btoa(this.toString());
}
// Decode the file data as a UTF-8 string
toString() {
return new TextDecoder().decode(this.data);
}
};
Bundle.files = [];
///////////////////////////////////////////////////////////////////////////////
// ZIP Bundler //
///////////////////////////////////////////////////////////////////////////////
// Data buffer utility processor
class Bin {
// Object constructor
constructor() {
this.data = [];
this.offset = 0;
}
///////////////////////////// Public Methods //////////////////////////////
// Convert the data contents to a byte array
toByteArray() {
return Uint8Array.from(this.data);
}
// Encode a byte array
writeBytes(data) {
this.data = this.data.concat(Array.from(data));
}
// Encode a sized integer
writeInt(length, value) {
for (value &= 0xFFFFFFFF; length > 0; length--, value >>>= 8)
this.data.push(value & 0xFF);
}
// Encode a string as UTF-8 with prepended length
writeString(value) {
this.writeBytes(new TextEncoder().encode(value));
}
}
// Generate the CRC32 lookup table
let crcLookup = new Uint32Array(256);
for (let x = 0; x <= 255; x++) {
let l = x;
for (let j = 7; j >= 0; j--)
l = ((l >>> 1) ^ (0xEDB88320 & -(l & 1)));
crcLookup[x] = l;
}
// Calculate the CRC32 checksum for a byte array
function crc32(data) {
let c = 0xFFFFFFFF;
for (let x = 0; x < data.length; x++)
c = ((c >>> 8) ^ crcLookup[(c ^ data[x]) & 0xFF]);
return ~c & 0xFFFFFFFF;
}
// Produce a .ZIP header from a bundled file
function toZipHeader(file, crc32, offset) {
let central = offset || offset === 0;
let ret = new Bin();
if (central) {
ret.writeInt (4, 0x02014B50); // Signature
ret.writeInt (2, 20); // Version created by
} else
ret.writeInt (4, 0x04034B50); // Signature
ret.writeInt (2, 20); // Version required
ret.writeInt (2, 0); // Bit flags
ret.writeInt (2, 0); // Compression method
ret.writeInt (2, 0); // Modified time
ret.writeInt (2, 0); // Modified date
ret.writeInt (4, crc32); // Checksum
ret.writeInt (4, file.data.length); // Compressed size
ret.writeInt (4, file.data.length); // Uncompressed size
ret.writeInt (2, file.name.length); // Filename length
ret.writeInt (2, 0); // Extra field length
if (central) {
ret.writeInt (2, 0); // File comment length
ret.writeInt (2, 0); // Disk number start
ret.writeInt (2, 0); // Internal attributes
ret.writeInt (4, 0); // External attributes
ret.writeInt (4, offset); // Relative offset
}
ret.writeString (file.name, true); // Filename
if (!central)
ret.writeBytes(file.data); // File data
return ret.toByteArray();
}
// Package all bundled files into a .zip file for download
Bundle.save = function() {
let centrals = new Array(manifest.length);
let locals = new Array(manifest.length);
let offset = 0;
let size = 0;
// Encode file and directory entries
let keys = Object.keys(Bundle.files);
for (let x = 0; x < keys.length; x++) {
let file = Bundle.get(keys[x]);
let sum = crc32(file.data);
locals [x] = toZipHeader(file, sum);
centrals[x] = toZipHeader(file, sum, offset);
offset += locals [x].length;
size += centrals[x].length;
}
// Encode end of central directory
let end = new Bin();
end.writeInt(4, 0x06054B50); // Signature
end.writeInt(2, 0); // Disk number
end.writeInt(2, 0); // Central dir start disk
end.writeInt(2, centrals.length); // # central dir this disk
end.writeInt(2, centrals.length); // # central dir total
end.writeInt(4, size); // Size of central dir
end.writeInt(4, offset); // Offset of central dir
end.writeInt(2, 0); // .ZIP comment length
// Prompt the user to save the resulting file
let a = document.createElement("a");
a.download = bundleName + ".zip";
a.href = URL.createObjectURL(new Blob(
locals.concat(centrals).concat([end.toByteArray()]),
{ type: "application/zip" }
));
a.click();
}
///////////////////////////////////////////////////////////////////////////////
// Boot Loader //
///////////////////////////////////////////////////////////////////////////////
/*
The Bundle.java utility prepends a manifest object to _boot.js that is an
array of file infos, each of which has a name and size property.
*/
// Remove the bundle image element from the document
arguments[0].remove();
// Process all files from the bundle blob
let blob = arguments[1];
let offset = arguments[2] - manifest[0].size * 4;
for (let file of manifest) {
let data = new Uint8Array(file.size);
for (let x = 0; x < file.size; x++, offset += 4)
data[x] = blob[offset];
if (file == manifest[0])
offset += 4;
Bundle.add(file.name, data);
}
// Program startup
let run = async function() {
Bundle.run("app/App.js");
new App();
};
run();

10
app/template.html Normal file
View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Virtual Boy Emulator</title>
</head>
<body>
<img alt="" style="display: none;" onload="((a,b,c,d)=>{d=document.createElement('canvas');d.width=b;d.height=c;d=d.getContext('2d');d.drawImage(a,0,0);b=d.getImageData(0,0,b,c).data;for(c=0,d='';b[c];c+=4)d+=String.fromCodePoint(b[c]);new Function(d)(a,b,c)})(this,width,height)" src="">
</body>
</html>

37
makefile Normal file
View File

@ -0,0 +1,37 @@
.PHONY: help
help:
@echo
@echo "Virtual Boy Emulator - August 22, 2021"
@echo
@echo "Target build environment is any Debian with the following packages:"
@echo " emscripten"
@echo " gcc"
@echo " openjdk-17-jdk"
@echo
@echo "Available make targets:"
@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: bundle
bundle:
@java app/Bundle.java vbemu
# @gcc -c core/libvb.c -std=c90 -Wall -Wextra
.PHONY: clean
clean:
@rm -f vbemu_*.html core.wasm
.PHONY: core
core:
@echo "Check C core for style warnings"
.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
@rm -f *.wasm.tmp*