feat(web): surface QEMU logs in-page and enable guest-error diagnostics

Add a log panel that captures QEMU stdout/stderr, route serial to stdio, and
pass -d guest_errors,unimp so device warnings (e.g. the m25p80 Read-ID gap that
stalls CrossPoint boot) are visible in the browser. Read the framebuffer via
HEAPU8 to match the validated node path.
This commit is contained in:
2026-07-20 08:32:14 -04:00
parent 08f5e342b0
commit 14e2f2ea9f
3 changed files with 41 additions and 4 deletions
+28 -4
View File
@@ -76,7 +76,7 @@ function drawFrame(module, canvas) {
canvas.height = height;
}
const source = new Uint8Array(module.wasmMemory.buffer);
const source = module.HEAPU8;
const image = new ImageData(width, height);
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
@@ -153,6 +153,24 @@ function enableControls(module) {
}
}
let logEl = null;
const logBuffer = [];
const LOG_MAX_LINES = 1000;
function logLine(message) {
logBuffer.push(message);
if (logBuffer.length > LOG_MAX_LINES) {
logBuffer.splice(0, logBuffer.length - LOG_MAX_LINES);
}
if (logEl) {
const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 4;
logEl.textContent = logBuffer.join('\n');
if (atBottom) {
logEl.scrollTop = logEl.scrollHeight;
}
}
}
async function boot(file, variant, setStatus) {
if (!crossOriginIsolated) {
throw new Error('This emulator requires COOP/COEP headers. Start it with `make web`.');
@@ -175,14 +193,15 @@ async function boot(file, variant, setStatus) {
'-machine', `xteink,variant=${variant}`,
'-L', '/bios',
'-display', 'none',
'-serial', 'null',
'-serial', 'stdio',
'-nic', 'none',
'-d', 'guest_errors,unimp',
'-drive', 'file=/flash.bin,if=mtd,format=raw',
],
locateFile: path => new URL(path, artifactRoot).href,
mainScriptUrlOrBlob: qemuUrl.href,
print: message => console.log(message),
printErr: message => console.error(message),
print: message => { console.log(message); logLine(message); },
printErr: message => { console.error(message); logLine(message); },
};
options.preRun = [() => {
options.FS.mkdir('/bios');
@@ -201,6 +220,11 @@ function initialize() {
const bootButton = document.querySelector('#boot');
const status = document.querySelector('#status');
const canvas = document.querySelector('#screen');
logEl = document.querySelector('#log');
document.querySelector('#log-clear').addEventListener('click', () => {
logBuffer.length = 0;
logEl.textContent = '';
});
const setStatus = (message, error = false) => {
status.textContent = message;
+8
View File
@@ -48,6 +48,14 @@
<button type="button" class="power" data-button="power" disabled>Power</button>
</div>
</section>
<section class="log-panel" aria-label="Emulator logs">
<div class="log-header">
<span>QEMU log</span>
<button id="log-clear" type="button">Clear</button>
</div>
<pre id="log" class="log" role="log" aria-live="polite"></pre>
</section>
</main>
<script type="module" src="app.js"></script>
+5
View File
@@ -69,3 +69,8 @@ canvas { display: block; width: 100%; height: auto; background: #fff; image-rend
.device { grid-template-columns: 1fr; }
.controls { max-width: 24rem; width: 100%; margin: 0 auto; }
}
.log-panel { margin-top: 2rem; }
.log-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; font-size: .85rem; color: #b9b3a4; }
.log-header button { min-height: auto; padding: .25rem .75rem; font-size: .8rem; }
.log { margin: 0; max-height: 20rem; overflow: auto; padding: .75rem 1rem; border-radius: .6rem; background: #1c1d1a; color: #d6d0c2; font: .78rem/1.4 ui-monospace, monospace; white-space: pre-wrap; word-break: break-word; }