Establishing web architecture
This commit is contained in:
parent
4b3852a1d2
commit
0927a42107
|
@ -0,0 +1,5 @@
|
|||
"use strict";
|
||||
|
||||
globalThis.App = class App {
|
||||
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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();
|
|
@ -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>
|
|
@ -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*
|
Loading…
Reference in New Issue