mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
Merge master (7230bd24: the WebGL GL backend removed) into merge/eject9
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01PeRutwFgZ66XpxjasKrPn1
This commit is contained in:
commit
2ddb25ce9b
17 files changed
+1399
-950
No files matched your search
@@ -198,6 +198,8 @@ run options:
|
||||
--verbose report translation statistics
|
||||
--env K=V add an environment variable (repeatable)
|
||||
--sysroot DIR present DIR as the guest's root filesystem
|
||||
--busybox FILE present FILE (a wasm32 busybox) as /bin/sh and the common
|
||||
coreutils in /bin and /usr/bin (GEX_BUSYBOX_APPLETS: the list)
|
||||
--pin FILE place each object where FILE says, as written by
|
||||
`gexc closure --pins`, and report where they landed
|
||||
--uid N[:M] the uid (and gid) the guest sees; `--uid 0` is root, which
|
||||
@@ -350,6 +352,11 @@ fn parse_run_args(args: &[String]) -> Result<RunArgs> {
|
||||
let v = args.get(i).context("--sysroot needs a directory")?;
|
||||
opts.sysroot = Some(v.into());
|
||||
}
|
||||
"--busybox" => {
|
||||
i += 1;
|
||||
let v = args.get(i).context("--busybox needs a wasm32 busybox")?;
|
||||
opts.busybox = Some(std::fs::canonicalize(v).with_context(|| format!("--busybox {v}"))?);
|
||||
}
|
||||
// A layout written by `gexc closure --pins`: every object is placed
|
||||
// exactly where the file says, so a module built against those
|
||||
// addresses is correct.
|
||||
@@ -605,6 +612,19 @@ fn learn_mem(ra: &RunArgs, bytes: &[u8], wide: bool) {
|
||||
fn cmd_run(args: &[String]) -> Result<ExitCode> {
|
||||
prof::start();
|
||||
let mut ra = parse_run_args(args)?;
|
||||
// `--busybox`: a program named as one of its applets (/bin/sh) is the
|
||||
// busybox the guest sees there, as the guest's own exec would find it.
|
||||
if let Some(bb) = &ra.opts.busybox {
|
||||
let base = ra.path.rsplit('/').next().unwrap_or(&ra.path).to_string();
|
||||
let applets = std::env::var("GEX_BUSYBOX_APPLETS").unwrap_or_else(|_| gex_native::BUSYBOX_APPLETS.join(","));
|
||||
let under = ra.path.starts_with("/bin/") || ra.path.starts_with("/usr/bin/");
|
||||
if under && applets.split(',').any(|a| a.trim() == base) {
|
||||
// (argv[0] stays the applet's name: it is what busybox runs)
|
||||
ra.argv0.get_or_insert_with(|| ra.path.clone());
|
||||
ra.path = bb.to_string_lossy().into_owned();
|
||||
ra.opts.exe = Some(ra.path.clone());
|
||||
}
|
||||
}
|
||||
let bytes = std::fs::read(&ra.path).with_context(|| format!("reading {}", ra.path))?;
|
||||
if gex_ld::pe::is_pe(&bytes) {
|
||||
// A Windows program: Wine's PE DLLs on gex's NT layer.
|
||||
|
||||
+32
-1
@@ -1073,6 +1073,35 @@ Steps:
|
||||
natively instead of translated. Map `/bin/sh` and the coreutils names to
|
||||
it in the guest root (natively by a gexc option, in the page's bundle).
|
||||
Biggest win per effort: every script exec today translates dash/coreutils.
|
||||
**State (2026-10-05, merge/wine-unix):** busybox 1.37.0 builds with
|
||||
`tools/wine-wasm/build-busybox.sh OUT` (config `busybox.config`: NOMMU,
|
||||
LFS, hush as sh, ~50 applets incl. sed/grep/awk/find/xargs, fancy tail;
|
||||
~400 KB of wasm). `gexc run --busybox B ...` mounts B read-only at
|
||||
`/bin/<applet>` and `/usr/bin/<applet>` for every applet
|
||||
(`BUSYBOX_APPLETS`; `GEX_BUSYBOX_APPLETS` overrides) and runs B for a
|
||||
program named `/bin/sh` etc.; `RunOptions.exe` makes `/proc/self/exe`
|
||||
busybox, which hush re-executes for `$(...)` and subshells.
|
||||
tests/wasm32/busybox.sh (needs `BUSYBOX`) compares pipelines, command
|
||||
substitution, redirections, externals and exit statuses with the host
|
||||
sh, and runs busybox under a translated x86-64 shell.
|
||||
- **vfork** (musl fork, `src/process/wasm32/__gexw_vfork.c`): gex cannot
|
||||
fork a wasm32 program, so `vfork()` is a setjmp macro (unistd.h); the
|
||||
"child" runs on the parent's stack with fds, cwd, umask, signal mask
|
||||
and actions snapshotted; its `execve` is a posix_spawn of that state
|
||||
(clone(VM|VFORK) is a new process) and its `_exit` spawns
|
||||
`/proc/self/exe` with `__GEXW_VFORK_EXIT=N` to exit with the status;
|
||||
then the parent's state is restored and vfork returns the pid. A child
|
||||
that does more than dup/close/chdir/signals before exec (hush's NOMMU
|
||||
paths re-exec, so busybox does not) would leak its changes into the
|
||||
parent's memory, as a real vfork would.
|
||||
- musl's `__libc_start_main` now calls `__wasm_call_ctors` (constructors
|
||||
never ran on wasm32 before).
|
||||
- Fixed on the way: a wasm32 child switched the host-wide translator
|
||||
compat (i386) mode, breaking an x86-64 parent after its wait
|
||||
(`wasmprog::is_wasm32`). A translated *i386* child of an x86-64 parent
|
||||
still has that limitation (documented in `run_program_with`).
|
||||
- Not yet: the page bundle's `/bin/sh` mapping (browser `--busybox`),
|
||||
and the prefix scripts' use of it.
|
||||
2. **ELF export providers** for glibc's hot entry points (`memcpy`/`memset`/
|
||||
`str*`, `malloc` family, libm), the PE providers' model on ELF images;
|
||||
needs gex-cgen's provider binding by ELF symbol (gex-cgen re-keys units:
|
||||
@@ -1182,7 +1211,9 @@ Steps:
|
||||
- W's check prefixes: `$W/pfx-native` (3 DLLs), `$W/pfx-dev` (9),
|
||||
`$W/pfx-gui` (11); `$W/pfx-w32u` is the coordinator's gate copy (do not
|
||||
restage while a gate runs). `$W/check.sh` runs them all.
|
||||
- Next: §5.1 (the guest's Unix userland as native wasm).
|
||||
- §5.1 item 1 (busybox as /bin/sh, natively) on merge/wine-unix; next:
|
||||
the browser mapping, then item 2 (ELF export providers: own branch,
|
||||
tell the coordinator before gex-cgen re-keys).
|
||||
- wine-w32's Makefile now uses wt/wine-crt's wcc (sed); merge into
|
||||
merge/wine-wasm after its gate.
|
||||
- **CRTs (plan, kept for reference):** native msvcrt needs an i386-semantics `except_wasm32.c`
|
||||
|
||||
@@ -2376,7 +2376,7 @@ export class Gex {
|
||||
}
|
||||
|
||||
/**
|
||||
* Give the guest a graphics device. `gl` is a `GlDevice` from web/src/gl.js.
|
||||
* Give the guest a graphics device. `gl` is a `MesaGlDevice` from web/src/mesa-gl.js.
|
||||
* Off by default, like the network. Best set before `load`, since the device
|
||||
* node is made while the filesystem is built; a later call still works.
|
||||
*/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// The guest's GL device, reached from a Worker. The WebGL `GlDevice`
|
||||
// (web/src/gl.js) is on the page -- it owns the canvas -- while the drawing
|
||||
// The guest's GL device, reached from a Worker. The `MesaGlDevice`
|
||||
// (web/src/mesa-gl.js) is on the page -- it owns the canvas -- while the drawing
|
||||
// guest is a Worker. `gl_submit` is a wasm import called synchronously from
|
||||
// inside an ioctl, so the
|
||||
// call crosses a `SharedArrayBuffer` with `Atomics.wait`: the Worker copies its
|
||||
@@ -25,7 +25,7 @@ const IDLE = 0;
|
||||
const REQUEST = 1;
|
||||
const REPLY = 2;
|
||||
|
||||
// Must match guest/gl/gexgl.c and web/src/gl.js.
|
||||
// Must match guest/gl/gexgl.c and web/src/gl-proto.js.
|
||||
const HEADER_BYTES = 32;
|
||||
const CMD_CAPACITY = 32 * 1024 * 1024;
|
||||
const REPLY_CAPACITY = 32 * 1024 * 1024;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! The guest's graphics device in a browser: `/dev/gex-gl`.
|
||||
//!
|
||||
//! Same protocol as the native device (the guest's `libGLESv2.so.2` is one
|
||||
//! binary), executed here by the page's WebGL 2 (OpenGL ES 3.0). Page side is
|
||||
//! `web/src/gl.js`. The import carries an address into guest memory rather than
|
||||
//! binary), executed here by Mesa (gex_wgpu over WebGPU). Page side is
|
||||
//! `web/src/mesa-gl.js`. The import carries an address into guest memory rather than
|
||||
//! a copy, so a frame costs one call out of wasm however many GL calls it holds.
|
||||
|
||||
use gex_kernel::errno::{Errno, SysResult};
|
||||
|
||||
+41
-5
@@ -121,6 +121,13 @@ pub struct RunOptions {
|
||||
/// Host directory presented as the guest's root. Without one the guest
|
||||
/// sees the host filesystem directly.
|
||||
pub sysroot: Option<std::path::PathBuf>,
|
||||
/// A native wasm32 busybox (docs/wine-native.md §5.1) presented as
|
||||
/// `/bin/sh` and the common coreutils in `/bin` and `/usr/bin`, so a
|
||||
/// script's shell and tools run natively instead of being translated.
|
||||
pub busybox: Option<std::path::PathBuf>,
|
||||
/// The program file when argv[0] does not name it (`--busybox` running
|
||||
/// `/bin/sh`): what `/proc/self/exe` is, which hush re-executes.
|
||||
pub exe: Option<String>,
|
||||
/// Who the guest thinks it is. `None` reports the host's own ids, which a
|
||||
/// passthrough filesystem needs for consistent stat/getuid answers.
|
||||
pub identity: Option<(u32, u32)>,
|
||||
@@ -186,6 +193,8 @@ impl Default for RunOptions {
|
||||
trace: false,
|
||||
verbose: false,
|
||||
sysroot: None,
|
||||
busybox: None,
|
||||
exe: None,
|
||||
identity: None,
|
||||
sample: false,
|
||||
pins: std::collections::HashMap::new(),
|
||||
@@ -698,9 +707,16 @@ fn run_program_with(
|
||||
// One setting per host process -- the native runner keeps one translator
|
||||
// for every guest process, so a mixed tree is not supported here (the
|
||||
// browser has a shim per process).
|
||||
gex_xlate::set_compat(program.compat());
|
||||
lockstep::set_compat(program.compat());
|
||||
gex_kernel::set_compat32(program.compat() && gex_abi::X86);
|
||||
//
|
||||
// A native wasm32 program has no guest code to decode and leaves these
|
||||
// alone: it is an i386 ELF (and its kernel speaks the i386 ABI, below),
|
||||
// but an x86-64 shell that runs it goes on translating its own code
|
||||
// after the wait.
|
||||
if !wasmprog::is_wasm32(program) {
|
||||
gex_xlate::set_compat(program.compat());
|
||||
lockstep::set_compat(program.compat());
|
||||
gex_kernel::set_compat32(program.compat() && gex_abi::X86);
|
||||
}
|
||||
ftab::selftest_init();
|
||||
|
||||
// A profile records where a previous run went, feeding `extra_entries` so
|
||||
@@ -848,8 +864,9 @@ fn run_program_with(
|
||||
// `/proc/self/exe` is the resolved program, not argv[0]: a shell that
|
||||
// re-executes itself relies on the difference.
|
||||
let exe_path = opts
|
||||
.args
|
||||
.first()
|
||||
.exe
|
||||
.as_ref()
|
||||
.or(opts.args.first())
|
||||
.and_then(|a| std::fs::canonicalize(a).ok())
|
||||
.map(|p| p.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| opts.args.first().cloned().unwrap_or_default());
|
||||
@@ -2198,6 +2215,15 @@ fn catch_up_table(store: &mut Store<HostState>) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What `--busybox` presents (the applets tools/wine-wasm/busybox.config
|
||||
/// builds).
|
||||
pub const BUSYBOX_APPLETS: &[&str] = &[
|
||||
"sh", "ash", "ls", "cat", "sort", "head", "tail", "tr", "wc", "mkdir", "cp", "mv", "rm", "rmdir", "date",
|
||||
"uname", "sed", "grep", "egrep", "fgrep", "awk", "find", "xargs", "env", "id", "basename", "dirname", "echo",
|
||||
"printf", "test", "[", "expr", "cut", "touch", "ln", "chmod", "pwd", "sleep", "true", "false", "readlink",
|
||||
"realpath", "uniq", "tee", "stat", "dd", "seq", "yes", "printenv", "which",
|
||||
];
|
||||
|
||||
/// Build the guest's namespace.
|
||||
///
|
||||
/// The layout follows what a container presents: a root (the host, or a
|
||||
@@ -2218,6 +2244,16 @@ fn mount_filesystems(
|
||||
}
|
||||
None => kernel.vfs.mount("/", Arc::new(HostFs::new("/"))),
|
||||
}
|
||||
if let Some(bb) = &opts.busybox {
|
||||
// Each name a file mount of the one binary: busybox picks its
|
||||
// applet by argv[0]. GEX_BUSYBOX_APPLETS replaces the list.
|
||||
let list = std::env::var("GEX_BUSYBOX_APPLETS").unwrap_or_else(|_| BUSYBOX_APPLETS.join(","));
|
||||
for name in list.split(',').map(str::trim).filter(|n| !n.is_empty()) {
|
||||
for dir in ["/bin", "/usr/bin"] {
|
||||
kernel.vfs.mount(&format!("{dir}/{name}"), Arc::new(HostFs::read_only(bb)));
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut devfs = DevFs::new();
|
||||
if opts.gl {
|
||||
// Named for what it is rather than as `/dev/dri/renderD128`: this is
|
||||
|
||||
@@ -29,6 +29,18 @@ use super::*;
|
||||
|
||||
pub const MAGIC: &[u8; 8] = b"GEXWASM\0";
|
||||
|
||||
/// Whether `program` is a native wasm32 one (its header, from the image's
|
||||
/// segments, before anything is placed).
|
||||
pub(crate) fn is_wasm32(program: &Program) -> bool {
|
||||
let at = program.exe.entry.wrapping_sub(0x40);
|
||||
program.exe.segments.iter().any(|s| {
|
||||
at >= s.vaddr && at + 8 <= s.vaddr + s.bytes.len() as u64 && {
|
||||
let o = (at - s.vaddr) as usize;
|
||||
&s.bytes[o..o + 8] == MAGIC
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// If the program just placed is a native wasm32 one, install its module
|
||||
/// in the function table at its entries. Returns whether it was.
|
||||
pub(crate) fn install(store: &mut Store<HostState>, memory: &gex_mem::GuestMemory, program: &Program) -> Result<bool> {
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
<!doctype html>
|
||||
<meta charset="utf-8">
|
||||
<title>gex: a guest's OpenGL on WebGL</title>
|
||||
<!--
|
||||
The smallest page that runs a guest and gives it a canvas.
|
||||
|
||||
Nothing here is about presentation; it exists so a headless browser can
|
||||
report what happened. The demo page in web/ is the one meant for people.
|
||||
|
||||
What it checks is the whole path: an aarch64 binary that calls eglInitialize
|
||||
and glDrawArrays, translated to wasm, whose GL library marshals into its own
|
||||
memory, handed to this page one batch at a time, executed on WebGL 2, drawn
|
||||
on the canvas below.
|
||||
-->
|
||||
<style>
|
||||
body { background: #14161c; color: #c8ccd4; font: 13px ui-monospace, monospace; }
|
||||
canvas { border: 1px solid #333; image-rendering: pixelated; }
|
||||
#log { white-space: pre-wrap; }
|
||||
</style>
|
||||
<canvas id="screen" width="320" height="240"></canvas>
|
||||
<pre id="log"></pre>
|
||||
|
||||
<script type="module">
|
||||
import { Gex, Env } from './gex.mjs';
|
||||
import { GlDevice } from './src/gl.js';
|
||||
|
||||
// The DOM is scraped by the runner, so everything it needs goes in one
|
||||
// element joined by bars -- newlines do not survive --dump-dom intact.
|
||||
const lines = [];
|
||||
const say = (s) => {
|
||||
lines.push(s);
|
||||
document.getElementById('log').textContent = lines.join('|');
|
||||
};
|
||||
|
||||
/// Tell the runner it is over. A page cannot be scraped reliably while it is
|
||||
/// still working -- the guest takes as long as it takes -- so it reports
|
||||
/// rather than being watched.
|
||||
const finish = () => {
|
||||
// A plain GET, not `sendBeacon`: that one POSTs, and a report that silently
|
||||
// goes to a method the server does not answer looks exactly like a page
|
||||
// that never finished.
|
||||
const body = encodeURIComponent(lines.join('|'));
|
||||
return fetch(`/result?log=${body}`, { keepalive: true }).catch(() => {});
|
||||
};
|
||||
|
||||
const prog = new URLSearchParams(location.search).get('prog') || 'gles-triangle';
|
||||
|
||||
/** Collects what the guest writes, and answers reads with nothing. */
|
||||
class Collect extends Env {
|
||||
constructor() { super(); this.text = ''; this.partial = ''; }
|
||||
write(fd, bytes) {
|
||||
const s = new TextDecoder().decode(bytes);
|
||||
this.text += s;
|
||||
// Reported as it arrives rather than at the end: if the guest stops
|
||||
// half way, what it managed to say is the only clue there is.
|
||||
this.partial += s;
|
||||
let nl;
|
||||
while ((nl = this.partial.indexOf('\n')) >= 0) {
|
||||
const line = this.partial.slice(0, nl).trim();
|
||||
this.partial = this.partial.slice(nl + 1);
|
||||
if (line) say(line);
|
||||
}
|
||||
}
|
||||
now() { return BigInt(Date.now()) * 1000000n; }
|
||||
}
|
||||
|
||||
async function bytes(url) {
|
||||
const r = await fetch(url);
|
||||
if (!r.ok) throw new Error(`${url}: ${r.status}`);
|
||||
return new Uint8Array(await r.arrayBuffer());
|
||||
}
|
||||
|
||||
try {
|
||||
const canvas = document.getElementById('screen');
|
||||
const gl = new GlDevice(canvas);
|
||||
if (!gl.ok) throw new Error('no WebGL 2 in this browser');
|
||||
say(`webgl ${gl.describe()}`);
|
||||
|
||||
const env = new Collect();
|
||||
const gex = new Gex(env);
|
||||
// Before load: the device node is made while the guest's filesystem is.
|
||||
gex.useGl(gl);
|
||||
|
||||
// The guest's own filesystem: its loader and the GL driver it links
|
||||
// against. musl searches /usr/lib by default, so nothing has to tell it
|
||||
// where these are.
|
||||
say('fetching');
|
||||
const files = {
|
||||
'/lib/ld-musl-aarch64.so.1': await bytes('/fs/ld-musl-aarch64.so.1'),
|
||||
'/usr/lib/libGLESv2.so.2': await bytes('/fs/libGLESv2.so.2'),
|
||||
'/usr/lib/libEGL.so.1': await bytes('/fs/libEGL.so.1'),
|
||||
};
|
||||
const args = [`/${prog}`, ...(prog === 'gles-gears' ? ['12'] : [])];
|
||||
|
||||
say('loading');
|
||||
const entry = await gex.load(
|
||||
await bytes('/gex/gex_browser.wasm'),
|
||||
await bytes(`/fs/${prog}.wasm`),
|
||||
await bytes(`/fs/${prog}`),
|
||||
files,
|
||||
args,
|
||||
{},
|
||||
);
|
||||
say(`entry ${entry}`);
|
||||
const status = await gex.run(entry);
|
||||
say(`exit ${status}`);
|
||||
|
||||
// The checks, which differ per program because what they draw does.
|
||||
let ok = false;
|
||||
if (prog === 'gles-triangle') {
|
||||
ok = /inside\s+0 255 0/.test(env.text) && /outside\s+0 0 255/.test(env.text);
|
||||
} else {
|
||||
const m = env.text.match(/covered (\d+)%/);
|
||||
const covered = m ? Number(m[1]) : -1;
|
||||
ok = /gl error 0/.test(env.text) && covered >= 5 && covered <= 60;
|
||||
}
|
||||
// And that something actually landed on the canvas, which is the part the
|
||||
// guest's own output cannot tell us.
|
||||
const px = new Uint8Array(4);
|
||||
gl.gl.readPixels(canvas.width >> 1, canvas.height >> 1, 1, 1,
|
||||
gl.gl.RGBA, gl.gl.UNSIGNED_BYTE, px);
|
||||
say(`canvas centre ${px[0]} ${px[1]} ${px[2]}`);
|
||||
const drawn = px[0] + px[1] + px[2] > 0;
|
||||
say(`RESULT ${ok && drawn ? 'ok' : 'failed'}`);
|
||||
} catch (e) {
|
||||
say(`error ${e && e.message ? e.message : e}`);
|
||||
say('RESULT failed');
|
||||
}
|
||||
await finish();
|
||||
</script>
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/sh
|
||||
# The guest's OpenGL, on a real browser's WebGL.
|
||||
#
|
||||
# tests/browser-gl/run.sh [work-dir]
|
||||
#
|
||||
# Node cannot run this one. The other browser tests use node because what they
|
||||
# check is the translated code, and V8 is V8 wherever it is; this checks that a
|
||||
# guest's GL calls reach a canvas, and node has no canvas. So it drives
|
||||
# headless Chromium and reads what the page printed.
|
||||
#
|
||||
# Two programs, both ordinary GLES2 built against the standard headers and
|
||||
# linked against the usual sonames:
|
||||
#
|
||||
# gles-triangle a green triangle over a blue clear, checked pixel by pixel
|
||||
# gles-gears the three gears, checked by how much of the frame they cover
|
||||
#
|
||||
# The whole point is that neither knows it is not talking to Mesa.
|
||||
set -eu
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
ROOT="$(cd "$HERE/../.." && pwd)"
|
||||
WORK="${1:-${TMPDIR:-/tmp}/gex-browser-gl}"
|
||||
GEXC="$ROOT/target/release/gexc"
|
||||
SHIM="$ROOT/target/wasm32-unknown-unknown/release/gex_browser.wasm"
|
||||
CC="${GEX_CC:-$HOME/toolchains/aarch64-linux-musl-cross/bin/aarch64-linux-musl-gcc}"
|
||||
MUSL="$(dirname "$CC")/../aarch64-linux-musl/lib/libc.so"
|
||||
CHROME="${GEX_CHROME:-$(command -v chromium || command -v chromium-browser || command -v google-chrome || true)}"
|
||||
|
||||
for need in "$GEXC" "$CC" "$CHROME"; do
|
||||
if [ -z "$need" ] || [ ! -x "$need" ]; then
|
||||
echo "skip: missing $need"
|
||||
exit 0
|
||||
fi
|
||||
done
|
||||
# Built through the shim's own script, not plain cargo: the memory has to be
|
||||
# imported, the globals rebased above the guest, and `__stack_pointer`
|
||||
# exported so the page can move the shim's stack out of the guest's image. A
|
||||
# module built without those loads and then fails on the first call.
|
||||
echo "== building the browser shim"
|
||||
"$ROOT/shims/browser/build.sh" >/dev/null 2>&1 || true
|
||||
[ -f "$SHIM" ] || { echo "skip: the browser shim did not build"; exit 0; }
|
||||
[ -d /usr/include/GLES2 ] || { echo "skip: no GLES2 headers"; exit 0; }
|
||||
|
||||
mkdir -p "$WORK/include"
|
||||
cp -r /usr/include/EGL /usr/include/GLES2 /usr/include/KHR "$WORK/include/" 2>/dev/null || true
|
||||
|
||||
echo "== building the guest's GL driver"
|
||||
"$ROOT/guest/gl/build.sh" "$WORK/driver" >/dev/null
|
||||
|
||||
echo "== building the programs"
|
||||
mkdir -p "$WORK/root/lib" "$WORK/root/usr/lib"
|
||||
cp "$MUSL" "$WORK/root/lib/ld-musl-aarch64.so.1"
|
||||
cp "$WORK/driver/libGLESv2.so.2" "$WORK/driver/libEGL.so.1" "$WORK/root/usr/lib/"
|
||||
for prog in gles-triangle gles-gears; do
|
||||
"$CC" -O2 -o "$WORK/root/$prog" "$ROOT/tests/integration/programs/$prog.c" \
|
||||
-I "$WORK/include" -L "$WORK/driver" -lEGL -lGLESv2 -lm
|
||||
done
|
||||
|
||||
# One module per program. `--no-shared-memory` because the page's runtime
|
||||
# declares a plain memory, and `--exhaustive` is not needed: the dispatch miss
|
||||
# pulls in what the guest actually reaches.
|
||||
echo "== translating"
|
||||
for prog in gles-triangle gles-gears; do
|
||||
"$GEXC" build --no-shared-memory "$WORK/root/$prog" -o "$WORK/$prog.wasm" >/dev/null
|
||||
done
|
||||
|
||||
# Everything the page loads has to be same-origin, so it is served rather than
|
||||
# opened from a file URL.
|
||||
mkdir -p "$WORK/site/gex" "$WORK/site/src" "$WORK/site/fs"
|
||||
cp "$SHIM" "$WORK/site/gex/gex_browser.wasm"
|
||||
cp "$ROOT/shims/browser/js/gex.mjs" "$WORK/site/"
|
||||
cp "$ROOT/web/src/gl.js" "$ROOT/web/src/gl-proto.js" "$WORK/site/src/"
|
||||
for prog in gles-triangle gles-gears; do
|
||||
cp "$WORK/$prog.wasm" "$WORK/root/$prog" "$WORK/site/fs/"
|
||||
done
|
||||
cp "$WORK/root/lib/ld-musl-aarch64.so.1" "$WORK/site/fs/"
|
||||
cp "$WORK/driver/libGLESv2.so.2" "$WORK/driver/libEGL.so.1" "$WORK/site/fs/"
|
||||
cp "$HERE/page.html" "$WORK/site/index.html"
|
||||
|
||||
port=$((18000 + $$ % 2000))
|
||||
pass=0
|
||||
fail=0
|
||||
|
||||
for prog in gles-triangle gles-gears; do
|
||||
echo "== running $prog in Chromium"
|
||||
out="$WORK/$prog.out"
|
||||
rm -f "$out"
|
||||
python3 "$HERE/serve.py" "$WORK/site" "$port" "$out" >/dev/null 2>&1 &
|
||||
server=$!
|
||||
for _ in $(seq 50); do
|
||||
curl -fsS "http://127.0.0.1:$port/index.html" >/dev/null 2>&1 && break
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# SwiftShader because no GPU here is reachable from the sandbox; the path
|
||||
# through WebGL is the same either way. No virtual-time budget: the page
|
||||
# says when it is finished, and this waits for it to.
|
||||
timeout 600 "$CHROME" --headless --no-sandbox --disable-gpu \
|
||||
--enable-unsafe-swiftshader \
|
||||
"http://127.0.0.1:$port/index.html?prog=$prog" >/dev/null 2>&1 &
|
||||
browser=$!
|
||||
for _ in $(seq 600); do
|
||||
[ -s "$out" ] && break
|
||||
sleep 1
|
||||
done
|
||||
kill $browser 2>/dev/null || true
|
||||
kill $server 2>/dev/null || true
|
||||
wait $browser 2>/dev/null || true
|
||||
wait $server 2>/dev/null || true
|
||||
|
||||
if [ ! -s "$out" ]; then
|
||||
echo "FAIL $prog: the page never reported"
|
||||
fail=$((fail + 1))
|
||||
continue
|
||||
fi
|
||||
sed 's/^/ /' "$out"
|
||||
if grep -q "^RESULT ok" "$out"; then
|
||||
pass=$((pass + 1))
|
||||
else
|
||||
echo "FAIL $prog"
|
||||
fail=$((fail + 1))
|
||||
fi
|
||||
port=$((port + 1))
|
||||
done
|
||||
|
||||
echo "$pass passed, $fail failed"
|
||||
[ "$fail" -eq 0 ]
|
||||
@@ -1,47 +0,0 @@
|
||||
"""A static server that also collects one result.
|
||||
|
||||
`--dump-dom` with a virtual-time budget is the obvious way to scrape a
|
||||
headless page, and it is wrong for this one: the budget expires on wall-clock
|
||||
idleness while the guest is still working, and what gets dumped is the page
|
||||
half way through. The guest here takes tens of seconds -- translation, then
|
||||
software rendering -- and how long is not predictable.
|
||||
|
||||
So the page says when it is done, by fetching `/result?...`, and the runner
|
||||
waits for that to arrive. No polling of the DOM, no guessing at a timeout,
|
||||
and a run that fails still reports because the failure is sent the same way.
|
||||
"""
|
||||
|
||||
import http.server
|
||||
import sys
|
||||
import urllib.parse
|
||||
|
||||
ROOT = sys.argv[1]
|
||||
PORT = int(sys.argv[2])
|
||||
OUT = sys.argv[3]
|
||||
|
||||
|
||||
class Handler(http.server.SimpleHTTPRequestHandler):
|
||||
def __init__(self, *a, **kw):
|
||||
super().__init__(*a, directory=ROOT, **kw)
|
||||
|
||||
def do_GET(self):
|
||||
if self.path.startswith("/result"):
|
||||
query = urllib.parse.urlparse(self.path).query
|
||||
body = urllib.parse.parse_qs(query).get("log", [""])[0]
|
||||
with open(OUT, "w", encoding="utf-8") as f:
|
||||
f.write(body.replace("|", "\n"))
|
||||
f.write("\n")
|
||||
self.send_response(204)
|
||||
self.end_headers()
|
||||
return
|
||||
super().do_GET()
|
||||
|
||||
# `sendBeacon` would POST; this answers both so a page reporting either
|
||||
# way is heard.
|
||||
do_POST = do_GET
|
||||
|
||||
def log_message(self, *a):
|
||||
pass
|
||||
|
||||
|
||||
http.server.ThreadingHTTPServer(("127.0.0.1", PORT), Handler).serve_forever()
|
||||
Executable
+43
@@ -0,0 +1,43 @@
|
||||
#!/bin/sh
|
||||
# A wasm32 busybox as the guest's /bin/sh and coreutils (`gexc run
|
||||
# --busybox`): a script with pipelines, command substitution, redirections,
|
||||
# externals and a failing command, against the same script under the
|
||||
# host's sh. BUSYBOX names the binary (tools/wine-wasm/build-busybox.sh);
|
||||
# without it this is a skip.
|
||||
#
|
||||
# tests/wasm32/busybox.sh GEXC
|
||||
set -u
|
||||
GEXC=$(cd "$(dirname "$1")" && pwd)/$(basename "$1")
|
||||
[ -n "${BUSYBOX:-}" ] && [ -f "$BUSYBOX" ] || { echo "skip busybox (set BUSYBOX)"; exit 0; }
|
||||
WORK=$(mktemp -d)
|
||||
cd "$WORK"
|
||||
printf 'b\na\nc\nb\n' > in.txt
|
||||
script='echo b a | tr " " "\n" | sort | head -1
|
||||
cat in.txt | sort | uniq | wc -l
|
||||
x=$(grep -c b in.txt); echo "b lines: $x"
|
||||
sed s/b/B/ in.txt | grep -c B
|
||||
mkdir -p d/e && cp in.txt d/e/ && ls d/e && rm -rf d
|
||||
for i in 1 2 3; do echo $i; done | tail -1
|
||||
awk "{n++} END {print n}" in.txt
|
||||
false || echo "false failed"
|
||||
/nonexistent-command 2>/dev/null; echo "status $?"
|
||||
echo done'
|
||||
native=$(sh -c "$script" 2>&1)
|
||||
gex=$("$GEXC" run --busybox "$BUSYBOX" /bin/sh -c "$script" 2>&1)
|
||||
if [ "$native" = "$gex" ]; then
|
||||
echo "ok busybox"
|
||||
else
|
||||
echo "FAIL busybox"
|
||||
printf '%s\n' "$native" > native.out; printf '%s\n' "$gex" > gex.out; diff native.out gex.out | head -20
|
||||
exit 1
|
||||
fi
|
||||
# An x86-64 shell (the host's, translated) running the wasm32 busybox and
|
||||
# going on afterwards: the child must not switch the translator to i386.
|
||||
mixed=$("$GEXC" run /bin/sh -c "$BUSYBOX echo hi | $BUSYBOX tr a-z A-Z; echo after" 2>&1)
|
||||
if [ "$mixed" = "$(printf 'HI\nafter')" ]; then
|
||||
echo "ok busybox under an x86-64 shell"
|
||||
else
|
||||
echo "FAIL busybox under an x86-64 shell"; printf '%s\n' "$mixed" | tail -5
|
||||
exit 1
|
||||
fi
|
||||
cd / && rm -rf "$WORK"
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/sh
|
||||
# busybox as a native wasm32 program (docs/wine-native.md §5.1): sh (hush)
|
||||
# and the common coreutils in one binary, for `gexc run --busybox`.
|
||||
#
|
||||
# tools/wine-wasm/build-busybox.sh OUT_DIR [VERSION] (GEX_WASM_SYSROOT set)
|
||||
#
|
||||
# Downloads busybox VERSION (default 1.37.0) into OUT_DIR, configures it
|
||||
# with busybox.config (NOMMU: vfork, which the musl fork's wasm32 vfork
|
||||
# emulates -- the child runs on the parent's stack until it execs or exits,
|
||||
# which become a posix_spawn; LFS; hush; tail -N), builds with wcc. Prints
|
||||
# the binary's path.
|
||||
set -eu
|
||||
here=$(cd "$(dirname "$0")" && pwd)
|
||||
out=$(mkdir -p "$1" && cd "$1" && pwd)
|
||||
ver=${2:-1.37.0}
|
||||
: "${GEX_WASM_SYSROOT:?set GEX_WASM_SYSROOT (tools/wine-wasm/build-sysroot.sh)}"
|
||||
src=$out/busybox-$ver
|
||||
if [ ! -d "$src" ]; then
|
||||
(cd "$out" && curl -sSLO "https://busybox.net/downloads/busybox-$ver.tar.bz2" && tar xjf "busybox-$ver.tar.bz2")
|
||||
fi
|
||||
cp "$here/busybox.config" "$src/.config"
|
||||
cd "$src"
|
||||
yes '' | make oldconfig >/dev/null 2>&1
|
||||
make -j"${JOBS:-6}" CC="$here/wcc" HOSTCC=gcc AR=llvm-ar STRIP=true SKIP_STRIP=y >"$out/build.log" 2>&1 \
|
||||
|| { tail -20 "$out/build.log" >&2; exit 1; }
|
||||
echo "$src/busybox"
|
||||
File diff suppressed because it is too large.
Load diff
+7
-14
@@ -413,15 +413,11 @@ const gears = {
|
||||
blurb:
|
||||
'An ordinary OpenGL ES 2 program. It includes the standard headers and links against ' +
|
||||
'libEGL and libGLESv2 by their usual names -- and those libraries, inside the guest, are ' +
|
||||
'marshallers: every call crosses to this page and runs on WebGL.',
|
||||
'marshallers: every call crosses to this page and runs on Mesa (gex_wgpu) over WebGPU.',
|
||||
network: false,
|
||||
|
||||
async enter(ctx) {
|
||||
const query = new URLSearchParams(location.search);
|
||||
// The Mesa path: same guest, same wire protocol, but st/mesa and the
|
||||
// gex_wgpu Gallium driver below the decoder instead of the page's WebGL.
|
||||
const mesaWebGpu = query.get('webgpu') === 'mesa' || query.get('gl') === 'mesa';
|
||||
const { GlDevice } = await import('./gl.js');
|
||||
|
||||
// The canvas lives above the terminal while this demo is chosen, made here
|
||||
// rather than in the markup so nothing else has to know the demo exists.
|
||||
@@ -434,13 +430,10 @@ const gears = {
|
||||
document.getElementById('terminal')?.before(wrap);
|
||||
ctx.onLeave(() => wrap.remove());
|
||||
|
||||
let gl;
|
||||
if (mesaWebGpu) {
|
||||
gl = await (await import('./mesa-gl.js')).MesaGlDevice.create(
|
||||
canvas, mesaBackend(query, 'es', 'wgpu'));
|
||||
} else {
|
||||
gl = new GlDevice(canvas);
|
||||
}
|
||||
// st/mesa and the gex_wgpu Gallium driver below the decoder: the only
|
||||
// browser GL backend (there is no WebGL path).
|
||||
const gl = await (await import('./mesa-gl.js')).MesaGlDevice.create(
|
||||
canvas, mesaBackend(query, 'es', 'wgpu'));
|
||||
if (!gl.ok) {
|
||||
ctx.term.writeln(ctx.ansi.red(gl.error || 'This browser has no usable GPU API.'));
|
||||
ctx.status('no GPU', 'bad');
|
||||
@@ -569,7 +562,7 @@ const gears = {
|
||||
};
|
||||
|
||||
ctx.actions([{ label: 'Run', primary: true, onClick: () => start(200) }]);
|
||||
ctx.hint('every GL call crosses to this page and runs on WebGL');
|
||||
ctx.hint('every GL call crosses to this page and runs on Mesa over WebGPU');
|
||||
|
||||
// `?frames=N` starts it straight away, so a link can carry the demo.
|
||||
const asked = Number(new URLSearchParams(location.search).get('frames'));
|
||||
@@ -1239,7 +1232,7 @@ const supertuxkart = {
|
||||
client: stkLaunch,
|
||||
intro: [
|
||||
'SuperTuxKart is running as an AArch64 X11 application inside this page:',
|
||||
'Xwayland -> gex-wayland; OpenGL -> gex-gl -> Mesa in this page -> WebGL or WebGPU.',
|
||||
'Xwayland -> gex-wayland; OpenGL -> gex-gl -> Mesa in this page -> WebGPU.',
|
||||
],
|
||||
hint: 'SuperTuxKart and Xwayland rendering straight to WebGPU',
|
||||
actions: (start) => {
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
// OpenGL 4.4 uncompressed pixel-transfer span calculation. This mirrors
|
||||
// guest/gl/gexgl-pixel.h; both sides must agree before either touches memory.
|
||||
|
||||
export function defaultPixelStore() {
|
||||
return { alignment: 4, rowLength: 0, imageHeight: 0,
|
||||
skipPixels: 0, skipRows: 0, skipImages: 0 };
|
||||
}
|
||||
|
||||
const mul = (a, b) => {
|
||||
const value = a * b;
|
||||
return Number.isSafeInteger(value) ? value : null;
|
||||
};
|
||||
const add = (a, b) => {
|
||||
const value = a + b;
|
||||
return Number.isSafeInteger(value) ? value : null;
|
||||
};
|
||||
|
||||
function components(format) {
|
||||
switch (format) {
|
||||
case 0x1900: case 0x1901: case 0x1902: case 0x1903:
|
||||
case 0x1904: case 0x1905: case 0x1906: case 0x1909:
|
||||
case 0x8d94: case 0x8d95: case 0x8d96: case 0x84f9: return 1;
|
||||
case 0x190a: case 0x8227: case 0x8228: return 2;
|
||||
case 0x1907: case 0x80e0: case 0x8d98: case 0x8d9a: return 3;
|
||||
case 0x1908: case 0x80e1: case 0x8d99: case 0x8d9b: return 4;
|
||||
default: return 0;
|
||||
}
|
||||
}
|
||||
|
||||
function groupBytes(format, type) {
|
||||
switch (type) {
|
||||
case 0x8032: case 0x8362: return 1;
|
||||
case 0x8033: case 0x8034: case 0x8363: case 0x8364:
|
||||
case 0x8365: case 0x8366: return 2;
|
||||
case 0x8035: case 0x8036: case 0x8367: case 0x8368:
|
||||
case 0x84fa: case 0x8c3b: case 0x8c3e: return 4;
|
||||
case 0x8dad: return 8;
|
||||
}
|
||||
const scalar = type === 0x1400 || type === 0x1401 ? 1
|
||||
: type === 0x1402 || type === 0x1403 || type === 0x140b ? 2
|
||||
: type === 0x1404 || type === 0x1405 || type === 0x1406 || type === 0x140c ? 4
|
||||
: type === 0x140a ? 8 : 0;
|
||||
return scalar * components(format);
|
||||
}
|
||||
|
||||
// depth === null means 2D; a non-negative number means 3D.
|
||||
export function pixelLayout(format, type, width, height, depth, store) {
|
||||
const is3d = depth !== null;
|
||||
if (!Number.isInteger(width) || !Number.isInteger(height) || width < 0 || height < 0 ||
|
||||
(is3d && (!Number.isInteger(depth) || depth < 0))) return null;
|
||||
if (!width || !height || (is3d && !depth))
|
||||
return { offset: 0, rowStride: 0, imageStride: 0, end: 0 };
|
||||
if (type === 0x1a00) return null; // GL_BITMAP needs bit-level transport.
|
||||
const group = groupBytes(format, type);
|
||||
if (!group) return null;
|
||||
const rowBytes = mul(store.rowLength || width, group);
|
||||
if (rowBytes === null) return null;
|
||||
const rowStride = Math.ceil(rowBytes / store.alignment) * store.alignment;
|
||||
if (!Number.isSafeInteger(rowStride)) return null;
|
||||
const imageStride = mul(rowStride, is3d && store.imageHeight ? store.imageHeight : height);
|
||||
const oi = mul(is3d ? store.skipImages : 0, imageStride);
|
||||
const or = mul(store.skipRows, rowStride);
|
||||
const op = mul(store.skipPixels, group);
|
||||
if ([imageStride, oi, or, op].some((value) => value === null)) return null;
|
||||
let offset = add(oi, or); offset = offset === null ? null : add(offset, op);
|
||||
const li = mul((is3d ? depth : 1) - 1, imageStride);
|
||||
const lr = mul(height - 1, rowStride);
|
||||
const active = mul(width, group);
|
||||
if ([offset, li, lr, active].some((value) => value === null)) return null;
|
||||
let end = add(offset, li); end = end === null ? null : add(end, lr);
|
||||
end = end === null ? null : add(end, active);
|
||||
return end === null ? null : { offset, rowStride, imageStride, end };
|
||||
}
|
||||
|
||||
export function setPixelStore(pack, unpack, pname, param) {
|
||||
if (!Number.isInteger(param) || param < 0) return false;
|
||||
let state, field;
|
||||
switch (pname) {
|
||||
case 0x0d05: state = pack; field = 'alignment'; break;
|
||||
case 0x0d02: state = pack; field = 'rowLength'; break;
|
||||
case 0x806c: state = pack; field = 'imageHeight'; break;
|
||||
case 0x0d04: state = pack; field = 'skipPixels'; break;
|
||||
case 0x0d03: state = pack; field = 'skipRows'; break;
|
||||
case 0x806b: state = pack; field = 'skipImages'; break;
|
||||
case 0x0cf5: state = unpack; field = 'alignment'; break;
|
||||
case 0x0cf2: state = unpack; field = 'rowLength'; break;
|
||||
case 0x806e: state = unpack; field = 'imageHeight'; break;
|
||||
case 0x0cf4: state = unpack; field = 'skipPixels'; break;
|
||||
case 0x0cf3: state = unpack; field = 'skipRows'; break;
|
||||
case 0x806d: state = unpack; field = 'skipImages'; break;
|
||||
default: return false;
|
||||
}
|
||||
if (field === 'alignment' && ![1, 2, 4, 8].includes(param)) return false;
|
||||
state[field] = param;
|
||||
return true;
|
||||
}
|
||||
-523
@@ -1,523 +0,0 @@
|
||||
// The browser's half of the graphics device: the guest marshals each GL call
|
||||
// into a buffer in its own memory, the shim hands this the address, and every
|
||||
// command becomes a real WebGL call on a real canvas.
|
||||
//
|
||||
// WebGL 2 *is* OpenGL ES 3.0, so almost nothing here is a translation. The two
|
||||
// exceptions are where the web refuses a raw pointer: object names (a table per
|
||||
// kind, indexed by the guest's integer) and buffer data (a subarray view into
|
||||
// guest memory, not a copy). The command encoding is generated from the Rust
|
||||
// definition in gl-proto.js, so guest, native host and this cannot drift apart.
|
||||
|
||||
import { op, MAGIC, PROTOCOL_VERSION, HEADER_BYTES, error } from './gl-proto.js';
|
||||
import { defaultPixelStore, pixelLayout, setPixelStore } from './gl-pixels.js';
|
||||
import { captureIfRequested } from './gl-capture.js';
|
||||
|
||||
// Header word offsets, in 32-bit units.
|
||||
const H_MAGIC = 0;
|
||||
const H_PROTOCOL_VERSION = 7;
|
||||
const H_CAPACITY = 1;
|
||||
const H_TAIL = 2;
|
||||
const H_REPLY_CAP = 3;
|
||||
const H_REPLY_LEN = 4;
|
||||
const H_STATUS = 5;
|
||||
const H_FAILED_AT = 6;
|
||||
|
||||
/// GL constants this file needs by name. The guest sends numbers, so these
|
||||
/// are only for the few places a decision is made.
|
||||
const GL_VENDOR = 0x1f00;
|
||||
const GL_RENDERER = 0x1f01;
|
||||
const GL_VERSION = 0x1f02;
|
||||
const GL_SHADING_LANGUAGE_VERSION = 0x8b8c;
|
||||
const GL_COMPILE_STATUS = 0x8b81;
|
||||
const GL_LINK_STATUS = 0x8b82;
|
||||
|
||||
/// Reads a command's arguments.
|
||||
class Args {
|
||||
constructor(view, start, end) {
|
||||
this.view = view;
|
||||
this.at = start;
|
||||
this.end = end;
|
||||
}
|
||||
u32() {
|
||||
if (this.at + 4 > this.end) return 0;
|
||||
const v = this.view.getUint32(this.at, true);
|
||||
this.at += 4;
|
||||
return v;
|
||||
}
|
||||
i32() {
|
||||
return this.u32() | 0;
|
||||
}
|
||||
f32() {
|
||||
if (this.at + 4 > this.end) return 0;
|
||||
const v = this.view.getFloat32(this.at, true);
|
||||
this.at += 4;
|
||||
return v;
|
||||
}
|
||||
u64() {
|
||||
const low = this.u32(), high = this.u32();
|
||||
const value = low + high * 0x100000000;
|
||||
return Number.isSafeInteger(value) ? value : 0;
|
||||
}
|
||||
/// A length word then that many bytes, as a view into guest memory rather
|
||||
/// than a copy: uploading a texture should not allocate.
|
||||
blob(bytes) {
|
||||
const len = this.u32();
|
||||
const start = this.at;
|
||||
if (start + len > this.end) return new Uint8Array(0);
|
||||
this.at += len;
|
||||
return bytes.subarray(start, start + len);
|
||||
}
|
||||
string(bytes) {
|
||||
// `.slice()` copies to a non-shared buffer first: the guest's memory is a
|
||||
// `SharedArrayBuffer` when threads are on (and always so through the gl
|
||||
// bridge), and `TextDecoder.decode` refuses a view onto shared memory.
|
||||
// `blob` itself stays a view -- WebGL's upload calls accept shared ones.
|
||||
return new TextDecoder().decode(this.blob(bytes).slice());
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the reply the guest reads back.
|
||||
class Reply {
|
||||
constructor() {
|
||||
this.parts = [];
|
||||
this.length = 0;
|
||||
}
|
||||
u32(v) {
|
||||
const b = new Uint8Array(4);
|
||||
new DataView(b.buffer).setUint32(0, v >>> 0, true);
|
||||
this.parts.push(b);
|
||||
this.length += 4;
|
||||
}
|
||||
blob(bytes) {
|
||||
this.u32(bytes.length);
|
||||
this.parts.push(bytes);
|
||||
this.length += bytes.length;
|
||||
const pad = (4 - (bytes.length % 4)) % 4;
|
||||
if (pad) {
|
||||
this.parts.push(new Uint8Array(pad));
|
||||
this.length += pad;
|
||||
}
|
||||
}
|
||||
string(s) {
|
||||
this.blob(new TextEncoder().encode(s));
|
||||
}
|
||||
into(dst) {
|
||||
let at = 0;
|
||||
for (const p of this.parts) {
|
||||
if (at + p.length > dst.length) break;
|
||||
dst.set(p, at);
|
||||
at += p.length;
|
||||
}
|
||||
return at;
|
||||
}
|
||||
}
|
||||
|
||||
export class GlDevice {
|
||||
/// `canvas` is where the guest's default framebuffer goes. A guest drawing
|
||||
/// to what it thinks is an offscreen pbuffer is drawing here, which is what
|
||||
/// makes the picture appear without anything having to copy it.
|
||||
constructor(canvas) {
|
||||
this.canvas = canvas;
|
||||
this.gl = canvas.getContext('webgl2', {
|
||||
alpha: false,
|
||||
antialias: false,
|
||||
depth: true,
|
||||
// The guest may read pixels back long after drawing them, and a page
|
||||
// that discards the buffer after compositing would hand back nothing.
|
||||
preserveDrawingBuffer: true,
|
||||
});
|
||||
this.buffers = new Map();
|
||||
this.textures = new Map();
|
||||
this.shaders = new Map();
|
||||
this.programs = new Map();
|
||||
this.framebuffers = new Map();
|
||||
this.packStore = defaultPixelStore();
|
||||
this.unpackStore = defaultPixelStore();
|
||||
this.ok = !!this.gl;
|
||||
this.capture = captureIfRequested(PROTOCOL_VERSION, canvas.width, canvas.height);
|
||||
}
|
||||
|
||||
describe() {
|
||||
if (!this.ok) return 'no WebGL 2';
|
||||
const gl = this.gl;
|
||||
return `${gl.getParameter(gl.VERSION)} / ${gl.getParameter(gl.RENDERER)}`;
|
||||
}
|
||||
|
||||
/// Run one batch. `memory` is the guest's WebAssembly.Memory and `ring` the
|
||||
/// address of the header inside it.
|
||||
///
|
||||
/// Everything is read through fresh views because the guest's memory can
|
||||
/// grow between calls, which detaches any buffer held from before.
|
||||
submit(memory, ring) {
|
||||
const bytes = new Uint8Array(memory.buffer);
|
||||
const words = new Uint32Array(memory.buffer, ring, HEADER_BYTES / 4);
|
||||
if (words[H_MAGIC] !== MAGIC) return -1;
|
||||
if (words[H_PROTOCOL_VERSION] !== PROTOCOL_VERSION) return -1;
|
||||
|
||||
const capacity = words[H_CAPACITY];
|
||||
const tail = words[H_TAIL];
|
||||
if (tail > capacity) return -1;
|
||||
|
||||
// Before execution: a batch that takes the context down with it is still
|
||||
// the batch a replay needs.
|
||||
this.capture?.record(
|
||||
bytes.subarray(ring + HEADER_BYTES, ring + HEADER_BYTES + tail),
|
||||
words[H_REPLY_CAP],
|
||||
);
|
||||
|
||||
const view = new DataView(memory.buffer);
|
||||
const base = ring + HEADER_BYTES;
|
||||
const reply = new Reply();
|
||||
let status = error.NONE;
|
||||
let index = 0;
|
||||
let at = 0;
|
||||
|
||||
while (at < tail) {
|
||||
if (at + 8 > tail) {
|
||||
status = error.TRUNCATED;
|
||||
break;
|
||||
}
|
||||
const opcode = view.getUint32(base + at, true);
|
||||
const len = view.getUint32(base + at + 4, true);
|
||||
if (len < 8 || at + len > tail) {
|
||||
status = error.TRUNCATED;
|
||||
break;
|
||||
}
|
||||
const args = new Args(view, base + at + 8, base + at + len);
|
||||
if (!this.one(opcode, args, bytes, reply)) {
|
||||
status = error.BAD_OPCODE;
|
||||
break;
|
||||
}
|
||||
at += len;
|
||||
index++;
|
||||
}
|
||||
|
||||
// Fresh views again: a command may have grown the guest's memory.
|
||||
const out = new Uint32Array(memory.buffer, ring, HEADER_BYTES / 4);
|
||||
const replyArea = new Uint8Array(
|
||||
memory.buffer,
|
||||
ring + HEADER_BYTES + capacity,
|
||||
out[H_REPLY_CAP],
|
||||
);
|
||||
const written = reply.into(replyArea);
|
||||
out[H_TAIL] = 0;
|
||||
out[H_REPLY_LEN] = written;
|
||||
out[H_STATUS] = status;
|
||||
out[H_FAILED_AT] = index;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// Execute one command. `false` means the opcode is unknown, after which
|
||||
/// the stream cannot be read any further.
|
||||
one(opcode, a, bytes, reply) {
|
||||
const gl = this.gl;
|
||||
switch (opcode) {
|
||||
case op.CONTEXT_CREATE:
|
||||
// The client API and version the guest asked for. This device is
|
||||
// WebGL 2 whatever the request, so they are read and not acted on --
|
||||
// but they must be consumed, because a command whose payload is left
|
||||
// unread is rejected as malformed.
|
||||
a.u32();
|
||||
a.u32();
|
||||
a.u32();
|
||||
reply.string(gl.getParameter(GL_VENDOR) ?? '');
|
||||
reply.string(gl.getParameter(GL_RENDERER) ?? '');
|
||||
reply.string(gl.getParameter(GL_VERSION) ?? '');
|
||||
break;
|
||||
case op.SURFACE_RESIZE: {
|
||||
const w = a.i32(), h = a.i32();
|
||||
// The guest asking for a pbuffer of some size is the closest thing
|
||||
// this design has to a request for a window, so the canvas takes it.
|
||||
if (w > 0 && h > 0) {
|
||||
this.canvas.width = w;
|
||||
this.canvas.height = h;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case op.FINISH:
|
||||
gl.finish();
|
||||
reply.u32(0);
|
||||
break;
|
||||
case op.PRESENT:
|
||||
// WebGL presents its default framebuffer implicitly. Keep the opcode
|
||||
// so explicit backends can use the same guest driver and wire stream.
|
||||
gl.flush();
|
||||
// The count of frames shown, which the guest paces against. Here that
|
||||
// is simply the count of presents processed -- the browser composites
|
||||
// this canvas on its own schedule and nothing defers it to a later
|
||||
// display() the way the WebGPU backend does -- so the guest is never
|
||||
// ahead and never throttles itself on this path.
|
||||
this.presents = (this.presents ?? 0) + 1;
|
||||
reply.u32(this.presents);
|
||||
break;
|
||||
|
||||
case op.CLEAR_COLOR: gl.clearColor(a.f32(), a.f32(), a.f32(), a.f32()); break;
|
||||
case op.CLEAR_DEPTH: gl.clearDepth(a.f32()); break;
|
||||
case op.CLEAR: gl.clear(a.u32()); break;
|
||||
case op.VIEWPORT: gl.viewport(a.i32(), a.i32(), a.i32(), a.i32()); break;
|
||||
case op.SCISSOR: gl.scissor(a.i32(), a.i32(), a.i32(), a.i32()); break;
|
||||
case op.ENABLE: gl.enable(a.u32()); break;
|
||||
case op.DISABLE: gl.disable(a.u32()); break;
|
||||
case op.BLEND_FUNC: gl.blendFunc(a.u32(), a.u32()); break;
|
||||
case op.DEPTH_FUNC: gl.depthFunc(a.u32()); break;
|
||||
case op.CULL_FACE: gl.cullFace(a.u32()); break;
|
||||
case op.FRONT_FACE: gl.frontFace(a.u32()); break;
|
||||
case op.PIXEL_STOREI: {
|
||||
const pname = a.u32(), param = a.i32();
|
||||
gl.pixelStorei(pname, param);
|
||||
setPixelStore(this.packStore, this.unpackStore, pname, param);
|
||||
break;
|
||||
}
|
||||
|
||||
case op.GEN_BUFFERS: {
|
||||
const n = a.u32();
|
||||
for (let i = 0; i < n; i++) this.buffers.set(a.u32(), gl.createBuffer());
|
||||
break;
|
||||
}
|
||||
case op.DELETE_BUFFERS: {
|
||||
const n = a.u32();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const name = a.u32();
|
||||
gl.deleteBuffer(this.buffers.get(name) ?? null);
|
||||
this.buffers.delete(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case op.BIND_BUFFER: {
|
||||
const target = a.u32(), name = a.u32();
|
||||
gl.bindBuffer(target, name ? (this.buffers.get(name) ?? null) : null);
|
||||
break;
|
||||
}
|
||||
case op.BUFFER_DATA: {
|
||||
const target = a.u32(), usage = a.u32(), size = a.u64(), hasData = a.u32() !== 0;
|
||||
const data = a.blob(bytes);
|
||||
if (!Number.isSafeInteger(size) || size < 0 ||
|
||||
(hasData ? data.length !== size : data.length !== 0)) { a.ok = false; break; }
|
||||
gl.bufferData(target, hasData ? data : size, usage);
|
||||
break;
|
||||
}
|
||||
case op.BUFFER_SUB_DATA: {
|
||||
const target = a.u32(), offset = a.u64(), size = a.u64(), data = a.blob(bytes);
|
||||
if (!Number.isSafeInteger(offset) || !Number.isSafeInteger(size) ||
|
||||
offset < 0 || size < 0 || data.length !== size) { a.ok = false; break; }
|
||||
gl.bufferSubData(target, offset, data);
|
||||
break;
|
||||
}
|
||||
|
||||
case op.CREATE_SHADER: {
|
||||
const name = a.u32(), type = a.u32();
|
||||
this.shaders.set(name, gl.createShader(type));
|
||||
break;
|
||||
}
|
||||
case op.SHADER_SOURCE: {
|
||||
const name = a.u32(), src = a.string(bytes);
|
||||
const s = this.shaders.get(name);
|
||||
if (s) gl.shaderSource(s, src);
|
||||
break;
|
||||
}
|
||||
case op.COMPILE_SHADER: {
|
||||
const s = this.shaders.get(a.u32());
|
||||
if (s) gl.compileShader(s);
|
||||
break;
|
||||
}
|
||||
case op.DELETE_SHADER: {
|
||||
const name = a.u32();
|
||||
gl.deleteShader(this.shaders.get(name) ?? null);
|
||||
this.shaders.delete(name);
|
||||
break;
|
||||
}
|
||||
case op.CREATE_PROGRAM:
|
||||
this.programs.set(a.u32(), gl.createProgram());
|
||||
break;
|
||||
case op.ATTACH_SHADER: {
|
||||
const p = this.programs.get(a.u32()), s = this.shaders.get(a.u32());
|
||||
if (p && s) gl.attachShader(p, s);
|
||||
break;
|
||||
}
|
||||
case op.BIND_ATTRIB_LOCATION: {
|
||||
const p = this.programs.get(a.u32());
|
||||
const index = a.u32(), name = a.string(bytes);
|
||||
if (p) gl.bindAttribLocation(p, index, name);
|
||||
break;
|
||||
}
|
||||
case op.LINK_PROGRAM: {
|
||||
const p = this.programs.get(a.u32());
|
||||
if (p) gl.linkProgram(p);
|
||||
break;
|
||||
}
|
||||
case op.USE_PROGRAM: {
|
||||
const name = a.u32();
|
||||
// Remembered because a uniform is set against whatever program is
|
||||
// current, and the integer the guest holds only means something
|
||||
// relative to that program's list.
|
||||
this.current = name ? (this.programs.get(name) ?? null) : null;
|
||||
gl.useProgram(this.current);
|
||||
break;
|
||||
}
|
||||
case op.DELETE_PROGRAM: {
|
||||
const name = a.u32();
|
||||
gl.deleteProgram(this.programs.get(name) ?? null);
|
||||
this.programs.delete(name);
|
||||
break;
|
||||
}
|
||||
case op.GET_UNIFORM_LOCATION: {
|
||||
const p = this.programs.get(a.u32()), name = a.string(bytes);
|
||||
// WebGL hands back an opaque location object, and the guest wants an
|
||||
// integer. They are kept in a list per program and the index stands
|
||||
// in for the location, which is all the guest ever does with it.
|
||||
if (!p) { reply.u32(0xffffffff); break; }
|
||||
if (!p._locations) p._locations = [];
|
||||
const loc = gl.getUniformLocation(p, name);
|
||||
if (loc === null) { reply.u32(0xffffffff); break; }
|
||||
p._locations.push(loc);
|
||||
reply.u32(p._locations.length - 1);
|
||||
break;
|
||||
}
|
||||
case op.GET_ATTRIB_LOCATION: {
|
||||
const p = this.programs.get(a.u32()), name = a.string(bytes);
|
||||
reply.u32(p ? gl.getAttribLocation(p, name) : -1);
|
||||
break;
|
||||
}
|
||||
case op.GET_SHADER_IV: {
|
||||
const s = this.shaders.get(a.u32()), pname = a.u32();
|
||||
const v = s ? gl.getShaderParameter(s, pname) : 0;
|
||||
reply.u32(pname === GL_COMPILE_STATUS ? (v ? 1 : 0) : (v | 0));
|
||||
break;
|
||||
}
|
||||
case op.GET_PROGRAM_IV: {
|
||||
const p = this.programs.get(a.u32()), pname = a.u32();
|
||||
const v = p ? gl.getProgramParameter(p, pname) : 0;
|
||||
reply.u32(pname === GL_LINK_STATUS ? (v ? 1 : 0) : (v | 0));
|
||||
break;
|
||||
}
|
||||
case op.GET_SHADER_INFO_LOG: {
|
||||
const s = this.shaders.get(a.u32());
|
||||
reply.string(s ? (gl.getShaderInfoLog(s) ?? '') : '');
|
||||
break;
|
||||
}
|
||||
case op.GET_PROGRAM_INFO_LOG: {
|
||||
const p = this.programs.get(a.u32());
|
||||
reply.string(p ? (gl.getProgramInfoLog(p) ?? '') : '');
|
||||
break;
|
||||
}
|
||||
|
||||
case op.UNIFORM_1I: gl.uniform1i(this.loc(a.i32()), a.i32()); break;
|
||||
case op.UNIFORM_1F: gl.uniform1f(this.loc(a.i32()), a.f32()); break;
|
||||
case op.UNIFORM_2F: gl.uniform2f(this.loc(a.i32()), a.f32(), a.f32()); break;
|
||||
case op.UNIFORM_3F:
|
||||
gl.uniform3f(this.loc(a.i32()), a.f32(), a.f32(), a.f32());
|
||||
break;
|
||||
case op.UNIFORM_4F:
|
||||
gl.uniform4f(this.loc(a.i32()), a.f32(), a.f32(), a.f32(), a.f32());
|
||||
break;
|
||||
case op.UNIFORM_MATRIX4FV: {
|
||||
const loc = this.loc(a.i32());
|
||||
const count = a.u32(), transpose = a.u32() !== 0;
|
||||
const data = a.blob(bytes);
|
||||
// A view rather than a copy, but it has to be aligned for Float32Array
|
||||
// and the guest's blob is only guaranteed word-aligned.
|
||||
const floats = new Float32Array(count * 16);
|
||||
new Uint8Array(floats.buffer).set(data.subarray(0, count * 64));
|
||||
gl.uniformMatrix4fv(loc, transpose, floats);
|
||||
break;
|
||||
}
|
||||
|
||||
case op.ENABLE_VERTEX_ATTRIB_ARRAY: gl.enableVertexAttribArray(a.u32()); break;
|
||||
case op.DISABLE_VERTEX_ATTRIB_ARRAY: gl.disableVertexAttribArray(a.u32()); break;
|
||||
case op.VERTEX_ATTRIB_POINTER: {
|
||||
const index = a.u32(), size = a.i32(), type = a.u32();
|
||||
const normalized = a.u32() !== 0, stride = a.i32(), offset = a.u32();
|
||||
gl.vertexAttribPointer(index, size, type, normalized, stride, offset);
|
||||
break;
|
||||
}
|
||||
|
||||
case op.GEN_TEXTURES: {
|
||||
const n = a.u32();
|
||||
for (let i = 0; i < n; i++) this.textures.set(a.u32(), gl.createTexture());
|
||||
break;
|
||||
}
|
||||
case op.DELETE_TEXTURES: {
|
||||
const n = a.u32();
|
||||
for (let i = 0; i < n; i++) {
|
||||
const name = a.u32();
|
||||
gl.deleteTexture(this.textures.get(name) ?? null);
|
||||
this.textures.delete(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
case op.BIND_TEXTURE: {
|
||||
const target = a.u32(), name = a.u32();
|
||||
gl.bindTexture(target, name ? (this.textures.get(name) ?? null) : null);
|
||||
break;
|
||||
}
|
||||
case op.ACTIVE_TEXTURE: gl.activeTexture(a.u32()); break;
|
||||
case op.TEX_PARAMETER_I: gl.texParameteri(a.u32(), a.u32(), a.i32()); break;
|
||||
case op.TEX_IMAGE_2D: {
|
||||
const target = a.u32(), level = a.i32(), internal = a.i32();
|
||||
const w = a.i32(), h = a.i32(), border = a.i32();
|
||||
const format = a.u32(), type = a.u32(), pbo = a.u32() !== 0;
|
||||
const pointer = a.u64(), data = a.blob(bytes);
|
||||
gl.texImage2D(target, level, internal, w, h, border, format, type,
|
||||
pbo ? pointer : (pointer ? data : null));
|
||||
break;
|
||||
}
|
||||
case op.TEX_SUB_IMAGE_2D: {
|
||||
const target = a.u32(), level = a.i32();
|
||||
const x = a.i32(), y = a.i32(), w = a.i32(), h = a.i32();
|
||||
const format = a.u32(), type = a.u32(), pbo = a.u32() !== 0;
|
||||
const pointer = a.u64(), data = a.blob(bytes);
|
||||
gl.texSubImage2D(target, level, x, y, w, h, format, type,
|
||||
pbo ? pointer : data);
|
||||
break;
|
||||
}
|
||||
|
||||
case op.DRAW_ARRAYS: gl.drawArrays(a.u32(), a.i32(), a.i32()); break;
|
||||
case op.DRAW_ELEMENTS: gl.drawElements(a.u32(), a.i32(), a.u32(), a.u32()); break;
|
||||
|
||||
case op.READ_PIXELS: {
|
||||
const x = a.i32(), y = a.i32(), w = a.i32(), h = a.i32();
|
||||
const format = a.u32(), type = a.u32(), pbo = a.u32() !== 0;
|
||||
const pointer = a.u64();
|
||||
if (pbo) {
|
||||
gl.readPixels(x, y, w, h, format, type, pointer);
|
||||
break;
|
||||
}
|
||||
const layout = pixelLayout(format, type, w, h, null, this.packStore);
|
||||
const px = new Uint8Array(layout?.end ?? 0);
|
||||
gl.readPixels(x, y, w, h, format, type, px);
|
||||
reply.u32(layout?.offset ?? 0);
|
||||
reply.blob(px.subarray(layout?.offset ?? 0));
|
||||
break;
|
||||
}
|
||||
case op.GET_STRING: {
|
||||
const name = a.u32();
|
||||
let s = gl.getParameter(name);
|
||||
if (name === GL_SHADING_LANGUAGE_VERSION && !s) s = '';
|
||||
reply.string(typeof s === 'string' ? s : String(s ?? ''));
|
||||
break;
|
||||
}
|
||||
case op.GET_INTEGERV: {
|
||||
const v = gl.getParameter(a.u32());
|
||||
const list = typeof v === 'number' ? [v] : Array.from(v ?? [0]);
|
||||
for (let i = 0; i < 4; i++) reply.u32(list[i] ?? 0);
|
||||
break;
|
||||
}
|
||||
case op.GET_ERROR:
|
||||
reply.u32(gl.getError());
|
||||
break;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/// Turn the integer the guest was given back into a WebGL uniform
|
||||
/// location, against whichever program is current -- which is the program
|
||||
/// the guest was looking at when it asked.
|
||||
loc(index) {
|
||||
const p = this.current;
|
||||
if (!p || !p._locations || index < 0) return null;
|
||||
return p._locations[index] ?? null;
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -467,7 +467,7 @@ export async function runProgram(
|
||||
// that never asked, and the guest's only symptom is an empty GL_VERSION
|
||||
// much later.
|
||||
console.warn(`[gex] runtime rings: gl=${!!gl} wl=${!!wl}`);
|
||||
// The GPU: a `GlDevice` (web/src/gl.js) the guest drives through
|
||||
// The GPU: a `MesaGlDevice` (web/src/mesa-gl.js) the guest drives through
|
||||
// libGLESv2/libEGL marshallers over a command ring. It needs a
|
||||
// SharedArrayBuffer bridge -- `gl_submit` is synchronous inside an
|
||||
// ioctl and the canvas is here, not in the Worker. Without this the `gl`
|
||||
|
||||
Reference in new issue
Block a user