From a87c9576fc203fc3b64732f15e9fa33d28f66a61 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Mon, 20 Jul 2026 14:25:43 -0400 Subject: [PATCH] working wasm --- Makefile | 23 +- qemu/patches/0002-qemu-wasm.patch | 404 +++++++++++++++++++----------- scripts/build-qemu-wasm.sh | 19 +- scripts/smoke-browser-bridge.mjs | 6 +- scripts/test-web.mjs | 12 +- web/app.js | 89 +++++-- web/index.html | 27 +- web/styles.css | 37 +-- 8 files changed, 399 insertions(+), 218 deletions(-) diff --git a/Makefile b/Makefile index f37f5e3..22433e6 100644 --- a/Makefile +++ b/Makefile @@ -9,7 +9,7 @@ QEMU_WASM_SRC ?= _scratch/qemu-wasm-src WASM_OUT ?= dist/wasm VARIANT ?= x3 -.PHONY: firmware sdimage qemu qemu-wasm web web-test run run-upstream chip clean +.PHONY: firmware sdimage blank-sd-asset qemu qemu-wasm web web-test run run-upstream chip clean firmware: esptool --chip esp32c3 merge-bin -o flash.bin \ @@ -23,11 +23,17 @@ firmware: sdimage: scripts/mksd.sh $(SD_SRC) sd.img +blank-sd-asset: + @tmpdir=$$(mktemp -d); trap 'rm -rf "$$tmpdir"' EXIT; \ + mkdir "$$tmpdir/empty"; \ + SIZE_MB=64 scripts/mksd.sh "$$tmpdir/empty" "$$tmpdir/sd.img"; \ + gzip -n -9 -c "$$tmpdir/sd.img" > web/assets/blank-sd.img.gz + qemu: QEMU_SRC=$(QEMU_SRC) scripts/build-qemu.sh qemu-wasm: - QEMU_WASM_SRC=$(QEMU_WASM_SRC) WASM_OUT=$(WASM_OUT) JOBS=2 scripts/build-qemu-wasm.sh + QEMU_WASM_SRC=$(QEMU_WASM_SRC) WASM_OUT=$(WASM_OUT) scripts/build-qemu-wasm.sh # Serve over HTTPS by default so headless/LAN access is cross-origin isolated. Override with WEB_TLS=. WEB_TLS ?= --tls @@ -39,9 +45,16 @@ web-test: # Select X4 with `make run VARIANT=x4`. Add `-display none` for headless. run: qemu - $(QEMU_BIN) -machine xteink,variant=$(VARIANT) -L $(QEMU_SRC)/pc-bios \ - -serial stdio -drive file=flash.bin,if=mtd,format=raw \ - $(if $(wildcard sd.img),-drive file=sd.img,if=sd,format=raw) + @sd=sd.img; tmpdir=; \ + if [ ! -f "$$sd" ]; then \ + tmpdir=$$(mktemp -d); mkdir "$$tmpdir/empty"; sd="$$tmpdir/sd.img"; \ + SIZE_MB=64 scripts/mksd.sh "$$tmpdir/empty" "$$sd" >/dev/null; \ + echo "Using a blank ephemeral SD card; writes will be discarded when QEMU exits."; \ + fi; \ + trap 'rm -rf "$$tmpdir"' EXIT; \ + $(QEMU_BIN) -machine xteink,variant=$(VARIANT) -L $(QEMU_SRC)/pc-bios \ + -serial stdio -drive file=flash.bin,if=mtd,format=raw \ + -drive file="$$sd",if=sd,format=raw # Unpatched release binary retained as the fast generic ESP32-C3 baseline. run-upstream: diff --git a/qemu/patches/0002-qemu-wasm.patch b/qemu/patches/0002-qemu-wasm.patch index 4e4f6ee..fc27fcd 100644 --- a/qemu/patches/0002-qemu-wasm.patch +++ b/qemu/patches/0002-qemu-wasm.patch @@ -1,7 +1,7 @@ -diff --git a/accel/tcg/cputlb.c b/accel/tcg/cputlb.c +diff --git c/accel/tcg/cputlb.c w/accel/tcg/cputlb.c index b76a4eac4e..a673eb2eb7 100644 ---- a/accel/tcg/cputlb.c -+++ b/accel/tcg/cputlb.c +--- c/accel/tcg/cputlb.c ++++ w/accel/tcg/cputlb.c @@ -225,7 +225,7 @@ static void tlb_mmu_resize_locked(CPUTLBDesc *desc, CPUTLBDescFast *fast, rate = desc->window_max_entries * 100 / old_size; @@ -11,10 +11,10 @@ index b76a4eac4e..a673eb2eb7 100644 } else if (rate < 30 && window_expired) { size_t ceil = pow2ceil(desc->window_max_entries); size_t expected_rate = desc->window_max_entries * 100 / ceil; -diff --git a/accel/tcg/ldst_common.c.inc b/accel/tcg/ldst_common.c.inc +diff --git c/accel/tcg/ldst_common.c.inc w/accel/tcg/ldst_common.c.inc index ebbf380d76..3ff0b97f27 100644 ---- a/accel/tcg/ldst_common.c.inc -+++ b/accel/tcg/ldst_common.c.inc +--- c/accel/tcg/ldst_common.c.inc ++++ w/accel/tcg/ldst_common.c.inc @@ -12,32 +12,52 @@ * Load helpers for tcg-ldst.h */ @@ -143,11 +143,41 @@ index ebbf380d76..3ff0b97f27 100644 } /* -diff --git a/accel/tcg/tb-maint.c b/accel/tcg/tb-maint.c -index cc0f5afd47..575be0e359 100644 ---- a/accel/tcg/tb-maint.c -+++ b/accel/tcg/tb-maint.c -@@ -854,7 +854,11 @@ static inline void tb_remove_from_jmp_list(TranslationBlock *orig, int n_orig) +diff --git c/accel/tcg/tb-maint.c w/accel/tcg/tb-maint.c +index cc0f5afd47..1095dfd912 100644 +--- c/accel/tcg/tb-maint.c ++++ w/accel/tcg/tb-maint.c +@@ -755,6 +755,18 @@ static void tb_remove(TranslationBlock *tb) + } + #endif /* CONFIG_USER_ONLY */ + ++#if !defined(CONFIG_TCG_INTERPRETER) && defined(EMSCRIPTEN) ++#include "../../tcg/wasm32.h" ++ ++static void wasm_remove_tb_instance(void *p, uint32_t hash, void *userp) ++{ ++ TranslationBlock *tb = p; ++ (void)hash; ++ (void)userp; ++ remove_tb(tb->tc.ptr); ++} ++#endif ++ + /* flush all the translation blocks */ + static void do_tb_flush(CPUState *cpu, run_on_cpu_data tb_flush_count) + { +@@ -771,6 +783,10 @@ static void do_tb_flush(CPUState *cpu, run_on_cpu_data tb_flush_count) + tcg_flush_jmp_cache(cpu); + } + ++#if defined(EMSCRIPTEN) && !defined(CONFIG_TCG_INTERPRETER) ++ qht_iter(&tb_ctx.htable, wasm_remove_tb_instance, NULL); ++ flush_tb_instances(); ++#endif + qht_reset_size(&tb_ctx.htable, CODE_GEN_HTABLE_SIZE); + tb_remove_all(); + +@@ -854,7 +870,11 @@ static inline void tb_remove_from_jmp_list(TranslationBlock *orig, int n_orig) void tb_reset_jump(TranslationBlock *tb, int n) { uintptr_t addr = (uintptr_t)(tb->tc.ptr + tb->jmp_reset_offset[n]); @@ -159,15 +189,7 @@ index cc0f5afd47..575be0e359 100644 } /* remove any jumps to the TB */ -@@ -875,10 +879,18 @@ static inline void tb_jmp_unlink(TranslationBlock *dest) - qemu_spin_unlock(&dest->jmp_lock); - } - -+#if !defined(CONFIG_TCG_INTERPRETER) && defined(EMSCRIPTEN) -+#include "../../tcg/wasm32.h" -+#endif -+ - static void tb_jmp_cache_inval_tb(TranslationBlock *tb) +@@ -879,6 +899,10 @@ static void tb_jmp_cache_inval_tb(TranslationBlock *tb) { CPUState *cpu; @@ -178,10 +200,10 @@ index cc0f5afd47..575be0e359 100644 if (tb_cflags(tb) & CF_PCREL) { /* A TB may be at any virtual address */ CPU_FOREACH(cpu) { -diff --git a/accel/tcg/tcg-accel-ops-mttcg.c b/accel/tcg/tcg-accel-ops-mttcg.c +diff --git c/accel/tcg/tcg-accel-ops-mttcg.c w/accel/tcg/tcg-accel-ops-mttcg.c index 49814ec4af..0715610d37 100644 ---- a/accel/tcg/tcg-accel-ops-mttcg.c -+++ b/accel/tcg/tcg-accel-ops-mttcg.c +--- c/accel/tcg/tcg-accel-ops-mttcg.c ++++ w/accel/tcg/tcg-accel-ops-mttcg.c @@ -61,12 +61,18 @@ static void mttcg_force_rcu(Notifier *notify, void *data) * variable current_cpu can be used deep in the code to find the * current CPUState for a given thread. @@ -202,10 +224,10 @@ index 49814ec4af..0715610d37 100644 assert(tcg_enabled()); g_assert(!icount_enabled()); -diff --git a/backends/meson.build b/backends/meson.build +diff --git c/backends/meson.build w/backends/meson.build index da714b93d1..9b88d22685 100644 ---- a/backends/meson.build -+++ b/backends/meson.build +--- c/backends/meson.build ++++ w/backends/meson.build @@ -12,8 +12,10 @@ system_ss.add([files( if host_os != 'windows' @@ -219,10 +241,10 @@ index da714b93d1..9b88d22685 100644 endif if host_os == 'linux' system_ss.add(files('hostmem-memfd.c')) -diff --git a/block/file-posix.c b/block/file-posix.c +diff --git c/block/file-posix.c w/block/file-posix.c index 90fa54352c..0fa9cc31b6 100644 ---- a/block/file-posix.c -+++ b/block/file-posix.c +--- c/block/file-posix.c ++++ w/block/file-posix.c @@ -110,6 +110,10 @@ #include #endif @@ -256,17 +278,17 @@ index 90fa54352c..0fa9cc31b6 100644 /* * parse_zone - Fill a zone descriptor -diff --git a/configs/devices/riscv32-softmmu/xteink.mak b/configs/devices/riscv32-softmmu/xteink.mak +diff --git c/configs/devices/riscv32-softmmu/xteink.mak w/configs/devices/riscv32-softmmu/xteink.mak new file mode 100644 index 0000000000..a9179b4119 --- /dev/null -+++ b/configs/devices/riscv32-softmmu/xteink.mak ++++ w/configs/devices/riscv32-softmmu/xteink.mak @@ -0,0 +1 @@ +CONFIG_RISCV_ESP32C3=y -diff --git a/configure b/configure +diff --git c/configure w/configure index 18336376bf..4a4b9e7754 100755 ---- a/configure -+++ b/configure +--- c/configure ++++ w/configure @@ -340,7 +340,9 @@ int main(void) { return 0; } EOF } @@ -298,10 +320,10 @@ index 18336376bf..4a4b9e7754 100755 ! test -d "$source_path/linux-user/include/host/$host_arch" || ! test -d "$source_path/common-user/host/$host_arch"; }; then error_exit "linux-user/include/host/$host_arch does not exist." \ -diff --git a/disas/disas-host.c b/disas/disas-host.c +diff --git c/disas/disas-host.c w/disas/disas-host.c index 8146fafe80..2ee0605dba 100644 ---- a/disas/disas-host.c -+++ b/disas/disas-host.c +--- c/disas/disas-host.c ++++ w/disas/disas-host.c @@ -42,7 +42,7 @@ static void initialize_debug_host(CPUDebug *s) #else s->info.endian = BFD_ENDIAN_LITTLE; @@ -311,10 +333,10 @@ index 8146fafe80..2ee0605dba 100644 s->info.print_insn = print_insn_tci; #elif defined(__i386__) s->info.mach = bfd_mach_i386_i386; -diff --git a/hw/adc/esp32c3_adc.c b/hw/adc/esp32c3_adc.c +diff --git c/hw/adc/esp32c3_adc.c w/hw/adc/esp32c3_adc.c index 89087429c7..4d38b3e13c 100644 ---- a/hw/adc/esp32c3_adc.c -+++ b/hw/adc/esp32c3_adc.c +--- c/hw/adc/esp32c3_adc.c ++++ w/hw/adc/esp32c3_adc.c @@ -27,7 +27,7 @@ static uint64_t esp32c3_adc_read(void *opaque, hwaddr addr, unsigned size) } if (addr == ADC_DATA1 || addr == ADC_DATA2) { @@ -324,11 +346,30 @@ index 89087429c7..4d38b3e13c 100644 : ADC_MAX; } return s->regs[addr / 4]; -diff --git a/hw/block/m25p80.c b/hw/block/m25p80.c -index 46dbe7c521..4f05caf34e 100644 ---- a/hw/block/m25p80.c -+++ b/hw/block/m25p80.c -@@ -856,28 +856,18 @@ static void complete_collecting_data(Flash *s) +diff --git c/hw/block/m25p80.c w/hw/block/m25p80.c +index 46dbe7c521..1ee6f8ce7e 100644 +--- c/hw/block/m25p80.c ++++ w/hw/block/m25p80.c +@@ -222,7 +222,8 @@ static const FlashPartInfo known_devices[] = { + { INFO("is25lp016d", 0x9d6015, 0, 64 << 10, 32, ER_4K) }, + { INFO("is25lp032", 0x9d6016, 0, 64 << 10, 64, ER_4K) }, + { INFO("is25lp064", 0x9d6017, 0, 64 << 10, 128, ER_4K) }, +- { INFO("is25lp128", 0x9d6018, 0, 64 << 10, 256, ER_4K) }, ++ { INFO("is25lp128", 0x9d6018, 0, 64 << 10, 256, ER_4K), ++ .sfdp_read = m25p80_sfdp_is25lp128 }, + { INFO("is25lp256", 0x9d6019, 0, 64 << 10, 512, ER_4K) }, + { INFO("is25wp032", 0x9d7016, 0, 64 << 10, 64, ER_4K) }, + { INFO("is25wp064", 0x9d7017, 0, 64 << 10, 128, ER_4K) }, +@@ -385,6 +386,8 @@ typedef enum { + READ_FSR = 0x70, + RDCR = 0x15, + RDSFDP = 0x5a, ++ PROGRAM_ERASE_SUSPEND = 0x75, ++ PROGRAM_ERASE_RESUME = 0x7a, + + READ = 0x03, + READ4 = 0x13, +@@ -856,28 +859,18 @@ static void complete_collecting_data(Flash *s) break; case RDID_90: case RDID_AB: @@ -367,10 +408,49 @@ index 46dbe7c521..4f05caf34e 100644 break; case RDSFDP: -diff --git a/hw/display/xteink_x3_eink.c b/hw/display/xteink_x3_eink.c +@@ -1414,6 +1407,9 @@ static void decode_new_cmd(Flash *s, uint32_t value) + } + break; + case NOP: ++ case PROGRAM_ERASE_SUSPEND: ++ case PROGRAM_ERASE_RESUME: ++ /* Program and erase complete synchronously in this model. */ + break; + case EN_4BYTE_ADDR: + s->four_bytes_address_mode = true; +diff --git c/hw/block/m25p80_sfdp.c w/hw/block/m25p80_sfdp.c +index a03a291a09..6e01178299 100644 +--- c/hw/block/m25p80_sfdp.c ++++ w/hw/block/m25p80_sfdp.c +@@ -479,3 +479,12 @@ static const uint8_t sfdp_is25wp256[] = { + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + }; + define_sfdp_read(is25wp256); ++ ++uint8_t m25p80_sfdp_is25lp128(uint32_t addr) ++{ ++ /* The sibling parts share SFDP data except for the density field. */ ++ if (addr == 0x37) { ++ return 0x07; ++ } ++ return m25p80_sfdp_is25wp256(addr); ++} +diff --git c/hw/block/m25p80_sfdp.h w/hw/block/m25p80_sfdp.h +index 35785686a0..e6bfdfa7e8 100644 +--- c/hw/block/m25p80_sfdp.h ++++ w/hw/block/m25p80_sfdp.h +@@ -28,6 +28,7 @@ uint8_t m25p80_sfdp_w25q512jv(uint32_t addr); + uint8_t m25p80_sfdp_w25q80bl(uint32_t addr); + uint8_t m25p80_sfdp_w25q01jvq(uint32_t addr); + ++uint8_t m25p80_sfdp_is25lp128(uint32_t addr); + uint8_t m25p80_sfdp_is25wp256(uint32_t addr); + + #endif +diff --git c/hw/display/xteink_x3_eink.c w/hw/display/xteink_x3_eink.c index b699abae4e..8a177e9159 100644 ---- a/hw/display/xteink_x3_eink.c -+++ b/hw/display/xteink_x3_eink.c +--- c/hw/display/xteink_x3_eink.c ++++ w/hw/display/xteink_x3_eink.c @@ -9,6 +9,60 @@ #include "hw/irq.h" #include "hw/display/xteink_x3_eink.h" @@ -448,10 +528,10 @@ index b699abae4e..8a177e9159 100644 dpy_gfx_update(s->console, 0, 0, 480, 800); } -diff --git a/hw/gpio/esp32_gpio.c b/hw/gpio/esp32_gpio.c +diff --git c/hw/gpio/esp32_gpio.c w/hw/gpio/esp32_gpio.c index 9ec8bd9244..754990968c 100644 ---- a/hw/gpio/esp32_gpio.c -+++ b/hw/gpio/esp32_gpio.c +--- c/hw/gpio/esp32_gpio.c ++++ w/hw/gpio/esp32_gpio.c @@ -38,10 +38,20 @@ static void esp32_gpio_drive_outputs(Esp32GpioState *s) } } @@ -493,10 +573,10 @@ index 9ec8bd9244..754990968c 100644 default: return 0; } -diff --git a/hw/misc/esp32c3_cache.c b/hw/misc/esp32c3_cache.c +diff --git c/hw/misc/esp32c3_cache.c w/hw/misc/esp32c3_cache.c index 1879b5c2df..e3f09cc51b 100644 ---- a/hw/misc/esp32c3_cache.c -+++ b/hw/misc/esp32c3_cache.c +--- c/hw/misc/esp32c3_cache.c ++++ w/hw/misc/esp32c3_cache.c @@ -58,7 +58,6 @@ static inline uint32_t esp32c3_read_mmu_value(ESP32C3CacheState *s, hwaddr reg_a static inline void esp32c3_write_mmu_value(ESP32C3CacheState *s, hwaddr reg_addr, uint32_t value) @@ -533,10 +613,10 @@ index 1879b5c2df..e3f09cc51b 100644 } static void esp32c3_cache_init(Object *obj) -diff --git a/hw/misc/esp_sha.c b/hw/misc/esp_sha.c +diff --git c/hw/misc/esp_sha.c w/hw/misc/esp_sha.c index c9fa2e610a..cc186595eb 100644 ---- a/hw/misc/esp_sha.c -+++ b/hw/misc/esp_sha.c +--- c/hw/misc/esp_sha.c ++++ w/hw/misc/esp_sha.c @@ -22,46 +22,71 @@ #define SHA_WARNING 0 #define SHA_DEBUG 0 @@ -626,10 +706,10 @@ index c9fa2e610a..cc186595eb 100644 .len = sizeof(struct sha512_state) }, }; -diff --git a/hw/riscv/esp32c3.c b/hw/riscv/esp32c3.c +diff --git c/hw/riscv/esp32c3.c w/hw/riscv/esp32c3.c index 2f1ccbfbb0..febc96bfe3 100644 ---- a/hw/riscv/esp32c3.c -+++ b/hw/riscv/esp32c3.c +--- c/hw/riscv/esp32c3.c ++++ w/hw/riscv/esp32c3.c @@ -57,6 +57,10 @@ #include "hw/sd/sd.h" #include "hw/net/can/esp32c3_twai.h" @@ -752,10 +832,10 @@ index 2f1ccbfbb0..febc96bfe3 100644 /* X3 exposes the fingerprint chips; X4 omits them so stock firmware's * all-NAK probe selects X4. The X4 panel is a blank protocol stub. */ -diff --git a/include/exec/exec-all.h b/include/exec/exec-all.h +diff --git c/include/exec/exec-all.h w/include/exec/exec-all.h index 2e4c4cc4b4..99e6da08c9 100644 ---- a/include/exec/exec-all.h -+++ b/include/exec/exec-all.h +--- c/include/exec/exec-all.h ++++ w/include/exec/exec-all.h @@ -453,7 +453,7 @@ void tb_invalidate_phys_range(tb_page_addr_t start, tb_page_addr_t last); void tb_set_jmp_target(TranslationBlock *tb, int n, uintptr_t addr); @@ -765,20 +845,20 @@ index 2e4c4cc4b4..99e6da08c9 100644 extern __thread uintptr_t tci_tb_ptr; # define GETPC() tci_tb_ptr #else -diff --git a/include/hw/gpio/esp32_gpio.h b/include/hw/gpio/esp32_gpio.h +diff --git c/include/hw/gpio/esp32_gpio.h w/include/hw/gpio/esp32_gpio.h index e2f52ad702..721f5de5df 100644 ---- a/include/hw/gpio/esp32_gpio.h -+++ b/include/hw/gpio/esp32_gpio.h +--- c/include/hw/gpio/esp32_gpio.h ++++ w/include/hw/gpio/esp32_gpio.h @@ -33,3 +33,5 @@ typedef struct Esp32GpioState { typedef struct Esp32GpioClass { SysBusDeviceClass parent_class; } Esp32GpioClass; + +void esp32_gpio_set_input_level(Esp32GpioState *s, int pin, bool level); -diff --git a/include/qemu/atomic.h b/include/qemu/atomic.h +diff --git c/include/qemu/atomic.h w/include/qemu/atomic.h index 7a3f2e6576..50dc235043 100644 ---- a/include/qemu/atomic.h -+++ b/include/qemu/atomic.h +--- c/include/qemu/atomic.h ++++ w/include/qemu/atomic.h @@ -75,7 +75,6 @@ #else # define ATOMIC_REG_SIZE sizeof(void *) @@ -856,10 +936,10 @@ index 7a3f2e6576..50dc235043 100644 qatomic_cmpxchg__nocheck(ptr, old, new); \ }) -diff --git a/include/qemu/cacheflush.h b/include/qemu/cacheflush.h +diff --git c/include/qemu/cacheflush.h w/include/qemu/cacheflush.h index ae20bcda73..3f414fb7c9 100644 ---- a/include/qemu/cacheflush.h -+++ b/include/qemu/cacheflush.h +--- c/include/qemu/cacheflush.h ++++ w/include/qemu/cacheflush.h @@ -19,7 +19,7 @@ * mappings of the same physical page(s). */ @@ -869,10 +949,10 @@ index ae20bcda73..3f414fb7c9 100644 static inline void flush_idcache_range(uintptr_t rx, uintptr_t rw, size_t len) { -diff --git a/include/qemu/int128.h b/include/qemu/int128.h +diff --git c/include/qemu/int128.h w/include/qemu/int128.h index 174bd7dafb..0305e262ef 100644 ---- a/include/qemu/int128.h -+++ b/include/qemu/int128.h +--- c/include/qemu/int128.h ++++ w/include/qemu/int128.h @@ -8,7 +8,7 @@ * But libffi does not support __int128_t, and therefore cannot pass * or return values of this type, force use of the Int128 struct. @@ -882,10 +962,10 @@ index 174bd7dafb..0305e262ef 100644 typedef __int128_t Int128; typedef __int128_t __attribute__((aligned(16))) Int128Aligned; -diff --git a/include/qemu/osdep.h b/include/qemu/osdep.h +diff --git c/include/qemu/osdep.h w/include/qemu/osdep.h index fdff07fd99..e95faa4478 100644 ---- a/include/qemu/osdep.h -+++ b/include/qemu/osdep.h +--- c/include/qemu/osdep.h ++++ w/include/qemu/osdep.h @@ -133,7 +133,7 @@ QEMU_EXTERN_C int daemon(int, int); #include #include @@ -920,10 +1000,10 @@ index fdff07fd99..e95faa4478 100644 struct iovec { void *iov_base; size_t iov_len; -diff --git a/include/tcg/helper-info.h b/include/tcg/helper-info.h +diff --git c/include/tcg/helper-info.h w/include/tcg/helper-info.h index 909fe73afa..c5064473ac 100644 ---- a/include/tcg/helper-info.h -+++ b/include/tcg/helper-info.h +--- c/include/tcg/helper-info.h ++++ w/include/tcg/helper-info.h @@ -9,7 +9,7 @@ #ifndef TCG_HELPER_INFO_H #define TCG_HELPER_INFO_H @@ -949,10 +1029,10 @@ index 909fe73afa..c5064473ac 100644 unsigned typemask : 32; unsigned flags : 8; -diff --git a/include/tcg/tcg-opc.h b/include/tcg/tcg-opc.h +diff --git c/include/tcg/tcg-opc.h w/include/tcg/tcg-opc.h index 546eb49c11..834757c3a4 100644 ---- a/include/tcg/tcg-opc.h -+++ b/include/tcg/tcg-opc.h +--- c/include/tcg/tcg-opc.h ++++ w/include/tcg/tcg-opc.h @@ -305,7 +305,7 @@ DEF(last_generic, 0, 0, 0, TCG_OPF_NOT_PRESENT) #include "tcg-target.opc.h" #endif @@ -962,10 +1042,10 @@ index 546eb49c11..834757c3a4 100644 /* These opcodes are only for use between the tci generator and interpreter. */ DEF(tci_movi, 1, 0, 1, TCG_OPF_NOT_PRESENT) DEF(tci_movl, 1, 0, 1, TCG_OPF_NOT_PRESENT) -diff --git a/include/tcg/tcg.h b/include/tcg/tcg.h +diff --git c/include/tcg/tcg.h w/include/tcg/tcg.h index 9b82844dab..2cdfe0617b 100644 ---- a/include/tcg/tcg.h -+++ b/include/tcg/tcg.h +--- c/include/tcg/tcg.h ++++ w/include/tcg/tcg.h @@ -1010,7 +1010,7 @@ static inline size_t tcg_current_code_size(TCGContext *s) #define TB_EXIT_IDXMAX 1 #define TB_EXIT_REQUESTED 3 @@ -975,10 +1055,10 @@ index 9b82844dab..2cdfe0617b 100644 uintptr_t tcg_qemu_tb_exec(CPUArchState *env, const void *tb_ptr); #else typedef uintptr_t tcg_prologue_fn(CPUArchState *env, const void *tb_ptr); -diff --git a/meson.build b/meson.build +diff --git c/meson.build w/meson.build index 7f28871776..6e558969d4 100644 ---- a/meson.build -+++ b/meson.build +--- c/meson.build ++++ w/meson.build @@ -45,9 +45,9 @@ genh = [] qapi_trace_events = [] @@ -1067,10 +1147,10 @@ index 7f28871776..6e558969d4 100644 # slirp < 4.7 is incompatible with CFI support in QEMU. This is because # it passes function pointers within libslirp as callbacks for timers. -diff --git a/meson_options.txt b/meson_options.txt +diff --git c/meson_options.txt w/meson_options.txt index 5eeaf3eee5..76f7a65b40 100644 ---- a/meson_options.txt -+++ b/meson_options.txt +--- c/meson_options.txt ++++ w/meson_options.txt @@ -34,7 +34,7 @@ option('fuzzing_engine', type : 'string', value : '', option('trace_file', type: 'string', value: 'trace', description: 'Trace file prefix for simple backend') @@ -1080,10 +1160,10 @@ index 5eeaf3eee5..76f7a65b40 100644 value: 'auto', description: 'coroutine backend to use') # Everything else can be set via --enable/--disable-* option -diff --git a/os-posix.c b/os-posix.c +diff --git c/os-posix.c w/os-posix.c index 43f9a43f3f..595fa47866 100644 ---- a/os-posix.c -+++ b/os-posix.c +--- c/os-posix.c ++++ w/os-posix.c @@ -148,11 +148,13 @@ static void change_process_uid(void) exit(1); } @@ -1098,10 +1178,10 @@ index 43f9a43f3f..595fa47866 100644 } else { if (setgroups(1, &user_gid) < 0) { error_report("Failed to setgroups(1, [%d])", -diff --git a/qom/object.c b/qom/object.c +diff --git c/qom/object.c w/qom/object.c index 9edc06d391..c3d657e6cb 100644 ---- a/qom/object.c -+++ b/qom/object.c +--- c/qom/object.c ++++ w/qom/object.c @@ -1197,7 +1197,8 @@ GSList *object_class_get_list(const char *implements_type, return list; } @@ -1121,10 +1201,10 @@ index 9edc06d391..c3d657e6cb 100644 } Object *object_ref(void *objptr) -diff --git a/scripts/meson-buildoptions.sh b/scripts/meson-buildoptions.sh +diff --git c/scripts/meson-buildoptions.sh w/scripts/meson-buildoptions.sh index a8066aab03..bd3ae90008 100644 ---- a/scripts/meson-buildoptions.sh -+++ b/scripts/meson-buildoptions.sh +--- c/scripts/meson-buildoptions.sh ++++ w/scripts/meson-buildoptions.sh @@ -80,7 +80,7 @@ meson_options_help() { printf "%s\n" ' --tls-priority=VALUE Default TLS protocol/cipher priority string' printf "%s\n" ' [NORMAL]' @@ -1134,10 +1214,10 @@ index a8066aab03..bd3ae90008 100644 printf "%s\n" ' --with-pkgversion=VALUE use specified string as sub-version of the' printf "%s\n" ' package' printf "%s\n" ' --with-suffix=VALUE Suffix for QEMU data/modules/config directories' -diff --git a/system/memory.c b/system/memory.c +diff --git c/system/memory.c w/system/memory.c index 85f6834cb3..3958314765 100644 ---- a/system/memory.c -+++ b/system/memory.c +--- c/system/memory.c ++++ w/system/memory.c @@ -1636,7 +1636,7 @@ bool memory_region_init_resizeable_ram(MemoryRegion *mr, return true; } @@ -1147,10 +1227,10 @@ index 85f6834cb3..3958314765 100644 bool memory_region_init_ram_from_file(MemoryRegion *mr, Object *owner, const char *name, -diff --git a/system/physmem.c b/system/physmem.c +diff --git c/system/physmem.c w/system/physmem.c index 75389064a8..addd5d0c4b 100644 ---- a/system/physmem.c -+++ b/system/physmem.c +--- c/system/physmem.c ++++ w/system/physmem.c @@ -1235,7 +1235,7 @@ long qemu_maxrampagesize(void) return pagesize; } @@ -1178,10 +1258,10 @@ index 75389064a8..addd5d0c4b 100644 } else if (block->fd >= 0) { qemu_ram_munmap(block->fd, block->host, block->max_length); close(block->fd); -diff --git a/tcg/meson.build b/tcg/meson.build +diff --git c/tcg/meson.build w/tcg/meson.build index 69ebb4908a..0894a577c2 100644 ---- a/tcg/meson.build -+++ b/tcg/meson.build +--- c/tcg/meson.build ++++ w/tcg/meson.build @@ -15,11 +15,18 @@ tcg_ss.add(files( 'tcg-op-vec.c', )) @@ -1201,10 +1281,10 @@ index 69ebb4908a..0894a577c2 100644 endif tcg_ss.add(when: libdw, if_true: files('debuginfo.c')) -diff --git a/tcg/region.c b/tcg/region.c +diff --git c/tcg/region.c w/tcg/region.c index 478ec051c4..41895c68fa 100644 ---- a/tcg/region.c -+++ b/tcg/region.c +--- c/tcg/region.c ++++ w/tcg/region.c @@ -568,7 +568,7 @@ static int alloc_code_gen_buffer_anon(size_t size, int prot, return prot; } @@ -1232,10 +1312,10 @@ index 478ec051c4..41895c68fa 100644 if (tcg_splitwx_diff == 0) { need_prot |= host_prot_read_exec(); } -diff --git a/tcg/tcg.c b/tcg/tcg.c +diff --git c/tcg/tcg.c w/tcg/tcg.c index 6dc3072f92..214e66e9d6 100644 ---- a/tcg/tcg.c -+++ b/tcg/tcg.c +--- c/tcg/tcg.c ++++ w/tcg/tcg.c @@ -131,6 +131,10 @@ static void tcg_out_goto_tb(TCGContext *s, int which); static void tcg_out_op(TCGContext *s, TCGOpcode opc, const TCGArg args[TCG_MAX_OP_ARGS], @@ -1734,12 +1814,12 @@ index 6dc3072f92..214e66e9d6 100644 return tcg_current_code_size(s); } -diff --git a/tcg/wasm32.c b/tcg/wasm32.c +diff --git c/tcg/wasm32.c w/tcg/wasm32.c new file mode 100644 -index 0000000000..dfcbf1493b +index 0000000000..55ad790f17 --- /dev/null -+++ b/tcg/wasm32.c -@@ -0,0 +1,1230 @@ ++++ w/tcg/wasm32.c +@@ -0,0 +1,1246 @@ +/* + * Tiny Code Generator for QEMU + * @@ -1794,7 +1874,7 @@ index 0000000000..dfcbf1493b + const import_vec_size = memory_v.getInt32(wasm_begin + wasm_size, true); + const import_vec_begin = wasm_begin + wasm_size + 4; + -+ const wasm = HEAP8.subarray(wasm_begin, wasm_begin + wasm_size); ++ const wasm = Uint8Array.from(HEAP8.subarray(wasm_begin, wasm_begin + wasm_size)); + var helper = {}; + for (var i = 0; i < import_vec_size / 4; i++) { + helper[i] = wasmTable.get(memory_v.getInt32(import_vec_begin + i * 4, true)); @@ -1821,6 +1901,19 @@ index 0000000000..dfcbf1493b + return 0; +}); + ++EM_JS(void, flush_tb_instances, (), { ++ const tb = Module.__wasm32_tb; ++ if (!tb) { ++ return; ++ } ++ const memory_v = new DataView(HEAP8.buffer); ++ const remove_n = memory_v.getInt32(tb.to_remove_instance_idx_ptr, true); ++ for (var i = 0; i < remove_n * 4; i += 4) { ++ removeFunction(memory_v.getInt32(tb.to_remove_instance_ptr + i, true)); ++ } ++ memory_v.setInt32(tb.to_remove_instance_idx_ptr, 0, true); ++}); ++ +__thread bool initdone = false; +__thread int cur_core_num = -1; +__thread int export_vec_off = -1; @@ -1838,6 +1931,9 @@ index 0000000000..dfcbf1493b + return; + } + *slot = 0; ++ if (to_remove_instance_idx == TO_REMOVE_INSTANCE_SIZE) { ++ flush_tb_instances(); ++ } + to_remove_instance[to_remove_instance_idx++] = f; +} + @@ -2970,12 +3066,12 @@ index 0000000000..dfcbf1493b +} + +#endif -diff --git a/tcg/wasm32.h b/tcg/wasm32.h +diff --git c/tcg/wasm32.h w/tcg/wasm32.h new file mode 100644 -index 0000000000..1861510f0b +index 0000000000..1527bf4875 --- /dev/null -+++ b/tcg/wasm32.h -@@ -0,0 +1,44 @@ ++++ w/tcg/wasm32.h +@@ -0,0 +1,46 @@ +#ifndef TCG_WASM32_H +#define TCG_WASM32_H + @@ -3015,16 +3111,18 @@ index 0000000000..1861510f0b + +void remove_tb(void *tb_ptr); + ++void flush_tb_instances(void); ++ +void init_wasm32(); + +#define INSTANTIATE_NUM 1500 + +#endif -diff --git a/tcg/wasm32/tcg-target-con-set.h b/tcg/wasm32/tcg-target-con-set.h +diff --git c/tcg/wasm32/tcg-target-con-set.h w/tcg/wasm32/tcg-target-con-set.h new file mode 100644 index 0000000000..f50a6793b7 --- /dev/null -+++ b/tcg/wasm32/tcg-target-con-set.h ++++ w/tcg/wasm32/tcg-target-con-set.h @@ -0,0 +1,15 @@ +/* + * C_On_Im(...) defines a constraint set with outputs and inputs. @@ -3041,22 +3139,22 @@ index 0000000000..f50a6793b7 +C_O2_I1(r, r, r) +C_O2_I2(r, r, r, r) +C_O2_I4(r, r, r, r, r, r) -diff --git a/tcg/wasm32/tcg-target-con-str.h b/tcg/wasm32/tcg-target-con-str.h +diff --git c/tcg/wasm32/tcg-target-con-str.h w/tcg/wasm32/tcg-target-con-str.h new file mode 100644 index 0000000000..56d7aff291 --- /dev/null -+++ b/tcg/wasm32/tcg-target-con-str.h ++++ w/tcg/wasm32/tcg-target-con-str.h @@ -0,0 +1,5 @@ +/* + * Define constraint letters for register sets: + * REGS(letter, register_mask) + */ +REGS('r', MAKE_64BIT_MASK(0, TCG_TARGET_NB_REGS)) -diff --git a/tcg/wasm32/tcg-target-reg-bits.h b/tcg/wasm32/tcg-target-reg-bits.h +diff --git c/tcg/wasm32/tcg-target-reg-bits.h w/tcg/wasm32/tcg-target-reg-bits.h new file mode 100644 index 0000000000..196395aef9 --- /dev/null -+++ b/tcg/wasm32/tcg-target-reg-bits.h ++++ w/tcg/wasm32/tcg-target-reg-bits.h @@ -0,0 +1,6 @@ +#ifndef TCG_TARGET_REG_BITS_H +#define TCG_TARGET_REG_BITS_H @@ -3064,11 +3162,11 @@ index 0000000000..196395aef9 +#define TCG_TARGET_REG_BITS 64 + +#endif -diff --git a/tcg/wasm32/tcg-target.c.inc b/tcg/wasm32/tcg-target.c.inc +diff --git c/tcg/wasm32/tcg-target.c.inc w/tcg/wasm32/tcg-target.c.inc new file mode 100644 index 0000000000..9e82297a3f --- /dev/null -+++ b/tcg/wasm32/tcg-target.c.inc ++++ w/tcg/wasm32/tcg-target.c.inc @@ -0,0 +1,3824 @@ +/* + * Tiny Code Generator for QEMU @@ -6894,11 +6992,11 @@ index 0000000000..9e82297a3f +static inline void tcg_target_qemu_prologue(TCGContext *s) +{ +} -diff --git a/tcg/wasm32/tcg-target.h b/tcg/wasm32/tcg-target.h +diff --git c/tcg/wasm32/tcg-target.h w/tcg/wasm32/tcg-target.h new file mode 100644 index 0000000000..7346c3a76e --- /dev/null -+++ b/tcg/wasm32/tcg-target.h ++++ w/tcg/wasm32/tcg-target.h @@ -0,0 +1,153 @@ +/* + * Tiny Code Generator for QEMU @@ -7053,10 +7151,10 @@ index 0000000000..7346c3a76e +#define TCG_TARGET_HAS_MEMORY_BSWAP 0 + +#endif /* TCG_TARGET_H */ -diff --git a/util/cacheflush.c b/util/cacheflush.c +diff --git c/util/cacheflush.c w/util/cacheflush.c index a08906155a..babbb644e5 100644 ---- a/util/cacheflush.c -+++ b/util/cacheflush.c +--- c/util/cacheflush.c ++++ w/util/cacheflush.c @@ -225,7 +225,7 @@ static void __attribute__((constructor)) init_cache_info(void) * Architecture (+ OS) specific cache flushing mechanisms. */ @@ -7066,11 +7164,11 @@ index a08906155a..babbb644e5 100644 /* Caches are coherent and do not require flushing; symbol inline. */ -diff --git a/util/coroutine-fiber.c b/util/coroutine-fiber.c +diff --git c/util/coroutine-fiber.c w/util/coroutine-fiber.c new file mode 100644 index 0000000000..2ce1575362 --- /dev/null -+++ b/util/coroutine-fiber.c ++++ w/util/coroutine-fiber.c @@ -0,0 +1,123 @@ +/* + * emscripten fiber coroutine initialization code @@ -7195,10 +7293,10 @@ index 0000000000..2ce1575362 + + return self && self->caller; +} -diff --git a/util/int128.c b/util/int128.c +diff --git c/util/int128.c w/util/int128.c index df6c6331bd..c442ff4da8 100644 ---- a/util/int128.c -+++ b/util/int128.c +--- c/util/int128.c ++++ w/util/int128.c @@ -144,7 +144,7 @@ Int128 int128_rems(Int128 a, Int128 b) return r; } @@ -7208,10 +7306,10 @@ index df6c6331bd..c442ff4da8 100644 Int128 int128_divu(Int128 a_s, Int128 b_s) { -diff --git a/util/meson.build b/util/meson.build +diff --git c/util/meson.build w/util/meson.build index 5d8bef9891..e107fbfb69 100644 ---- a/util/meson.build -+++ b/util/meson.build +--- c/util/meson.build ++++ w/util/meson.build @@ -11,7 +11,9 @@ if host_os != 'windows' endif util_ss.add(files('compatfd.c')) @@ -7223,10 +7321,10 @@ index 5d8bef9891..e107fbfb69 100644 freebsd_dep = [] if host_os == 'freebsd' freebsd_dep = util -diff --git a/util/mmap-alloc.c b/util/mmap-alloc.c +diff --git c/util/mmap-alloc.c w/util/mmap-alloc.c index ed14f9c64d..8c8708ca4d 100644 ---- a/util/mmap-alloc.c -+++ b/util/mmap-alloc.c +--- c/util/mmap-alloc.c ++++ w/util/mmap-alloc.c @@ -250,6 +250,11 @@ void *qemu_ram_mmap(int fd, uint32_t qemu_map_flags, off_t map_offset) @@ -7247,10 +7345,10 @@ index ed14f9c64d..8c8708ca4d 100644 } void qemu_ram_munmap(int fd, void *ptr, size_t size) -diff --git a/util/oslib-posix.c b/util/oslib-posix.c +diff --git c/util/oslib-posix.c w/util/oslib-posix.c index 11b35e48fb..760c3d4f4c 100644 ---- a/util/oslib-posix.c -+++ b/util/oslib-posix.c +--- c/util/oslib-posix.c ++++ w/util/oslib-posix.c @@ -58,6 +58,7 @@ #include #endif diff --git a/scripts/build-qemu-wasm.sh b/scripts/build-qemu-wasm.sh index 86c6a34..f05b858 100755 --- a/scripts/build-qemu-wasm.sh +++ b/scripts/build-qemu-wasm.sh @@ -10,7 +10,7 @@ TAG="esp-develop-9.2.2-20260417" COMMIT="40edccac415693c5130f91c01d84176ae6008566" QEMU_WASM_COMMIT="0ef7b4e2814b231705d8371dd7997f5b72e70baf" IMAGE="xteink-qemu-wasm-build" -JOBS="${JOBS:-2}" +JOBS="${JOBS:-$(nproc 2>/dev/null || echo 2)}" if [ -n "${CONTAINER_ENGINE:-}" ]; then ENGINE="$CONTAINER_ENGINE" @@ -30,19 +30,22 @@ if [ "$(git rev-parse HEAD)" != "$COMMIT" ]; then exit 1 fi -if [ ! -f hw/display/xteink_x3_eink.c ] || [ ! -f tcg/wasm32.c ]; then - if ! git diff --quiet || ! git diff --cached --quiet; then - echo "QEMU source has changes that conflict with the WASM patches" >&2 - exit 1 - fi +# Re-apply Patches On Drift - The source tree is a build artifact of the patches; reset and re-apply whenever a patch is newer than the last apply so `make web` never silently rebuilds stale sources. +STAMP=".xteink-patch-stamp" +if [ ! -f hw/display/xteink_x3_eink.c ] || [ ! -f tcg/wasm32.c ] \ + || [ "$ROOT/qemu/patches/0001-xteink-machine.patch" -nt "$STAMP" ] \ + || [ "$ROOT/qemu/patches/0002-qemu-wasm.patch" -nt "$STAMP" ]; then + git reset --hard "$COMMIT" + git clean -fdx -e build git apply "$ROOT/qemu/patches/0001-xteink-machine.patch" git apply --whitespace=nowarn "$ROOT/qemu/patches/0002-qemu-wasm.patch" + touch "$STAMP" fi printf 'Building Emscripten environment (qemu-wasm %s)\n' "$QEMU_WASM_COMMIT" "$ENGINE" build -t "$IMAGE" -f "$ROOT/qemu/wasm/Dockerfile" "$ROOT/qemu/wasm" -FLAGS="-O3 -Wno-error=unused-command-line-argument -matomics -mbulk-memory -DNDEBUG -DG_DISABLE_ASSERT -D_GNU_SOURCE -sASYNCIFY=1 -pthread -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM -sALLOW_TABLE_GROWTH -sINITIAL_MEMORY=512MB -sWASM_BIGINT -sMALLOC=mimalloc -sEXPORT_ES6=1 -sASYNCIFY_IMPORTS=ffi_call_js" +FLAGS="-O3 -Wno-error=unused-command-line-argument -matomics -mbulk-memory -DNDEBUG -DG_DISABLE_ASSERT -D_GNU_SOURCE -sASYNCIFY=1 -pthread -sPROXY_TO_PTHREAD=1 -sFORCE_FILESYSTEM -sALLOW_TABLE_GROWTH -sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1GB -sSTACK_SIZE=1048576 -sWASM_BIGINT -sMALLOC=mimalloc -sEXPORT_ES6=1 -sASYNCIFY_IMPORTS=ffi_call_js" if [ ! -f build/build.ninja ]; then "$ENGINE" run --rm -v "$SRC:/qemu" "$IMAGE" bash -lc " @@ -62,7 +65,7 @@ if [ ! -f build/build.ninja ]; then --disable-tools \\ --extra-cflags='$FLAGS' \\ --extra-cxxflags='$FLAGS' \\ - --extra-ldflags='-sEXPORTED_RUNTIME_METHODS=getTempRet0,setTempRet0,addFunction,removeFunction,TTY,FS' + --extra-ldflags='-sINITIAL_MEMORY=64MB -sALLOW_MEMORY_GROWTH=1 -sMAXIMUM_MEMORY=1GB -sEXPORTED_RUNTIME_METHODS=getTempRet0,setTempRet0,addFunction,removeFunction,TTY,FS' " fi diff --git a/scripts/smoke-browser-bridge.mjs b/scripts/smoke-browser-bridge.mjs index 7874b17..2402705 100644 --- a/scripts/smoke-browser-bridge.mjs +++ b/scripts/smoke-browser-bridge.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { readFile } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; +import { gunzipSync } from 'node:zlib'; const artifactRoot = process.argv[2]; if (!artifactRoot) { @@ -9,12 +10,14 @@ if (!artifactRoot) { const moduleUrl = pathToFileURL(`${artifactRoot}/qemu-system-riscv32.js`); const rom = new Uint8Array(await readFile(`${artifactRoot}/esp32c3-rom.bin`)); +const sdCard = gunzipSync(await readFile('web/assets/blank-sd.img.gz')); const { default: createQemu } = await import(moduleUrl); const options = { arguments: [ - '-S', '-machine', 'xteink,variant=x3', '-L', '/bios', + '-S', '-machine', 'xteink,variant=x3', '-accel', 'tcg,tb-size=16', '-L', '/bios', '-display', 'none', '-serial', 'null', '-nic', 'none', '-drive', 'file=/flash.bin,if=mtd,format=raw', + '-drive', 'file=/sd.img,if=sd,format=raw', ], mainScriptUrlOrBlob: moduleUrl.href, }; @@ -22,6 +25,7 @@ options.preRun = [() => { options.FS.mkdir('/bios'); options.FS.writeFile('/bios/esp32c3-rom.bin', rom); options.FS.writeFile('/flash.bin', new Uint8Array(16 * 1024 * 1024)); + options.FS.writeFile('/sd.img', sdCard); }]; const module = await createQemu(options); diff --git a/scripts/test-web.mjs b/scripts/test-web.mjs index 31e1499..7865907 100644 --- a/scripts/test-web.mjs +++ b/scripts/test-web.mjs @@ -1,5 +1,6 @@ import assert from 'node:assert/strict'; -import { BUTTONS, setButton, mergeFirmware } from '../web/app.js'; +import { readFile } from 'node:fs/promises'; +import { BUTTONS, setButton, mergeFirmware, loadEphemeralSdCard } from '../web/app.js'; // Merge - app image is placed at 0x10000 in a 0xFF-erased 16 MB flash with bootloader and partitions. const bootloader = new Uint8Array([1, 2, 3]); @@ -14,6 +15,15 @@ assert.equal(flash[0x10000], 0xe9); assert.equal(flash[0x20000], 0xff); assert.throws(() => mergeFirmware(new Uint8Array([0x00]), bootloader, partitions), /0xE9/); +const compressedSd = await readFile('web/assets/blank-sd.img.gz'); +const fetchSd = async () => new Response(compressedSd); +const firstSd = await loadEphemeralSdCard(import.meta.url, fetchSd); +const secondSd = await loadEphemeralSdCard(import.meta.url, fetchSd); +assert.equal(firstSd.length, 64 * 1024 * 1024); +assert.deepEqual(firstSd.slice(510, 512), new Uint8Array([0x55, 0xaa])); +firstSd[0] ^= 0xff; +assert.notEqual(firstSd[0], secondSd[0]); + const calls = []; const module = { _xteink_wasm_set_adc: (...args) => calls.push(['adc', ...args]), diff --git a/web/app.js b/web/app.js index 1823952..3eadb2d 100644 --- a/web/app.js +++ b/web/app.js @@ -38,6 +38,18 @@ async function fetchBin(url) { return new Uint8Array(await response.arrayBuffer()); } +export async function loadEphemeralSdCard(assetRoot = import.meta.url, fetchFn = fetch) { + const response = await fetchFn(new URL('assets/blank-sd.img.gz', assetRoot)); + if (!response.ok) { + throw new Error(`Could not load blank SD card (${response.status}).`); + } + if (!response.body || typeof DecompressionStream === 'undefined') { + throw new Error('This browser cannot decompress the blank SD card image.'); + } + const stream = response.body.pipeThrough(new DecompressionStream('gzip')); + return new Uint8Array(await new Response(stream).arrayBuffer()); +} + // A full 16 MB image is used as-is; anything else is treated as a CrossPoint app image and merged with the bundled bootloader and partition table. async function prepareFirmware(bytes, assetRoot) { if (bytes.length === FLASH_SIZE) { @@ -154,14 +166,19 @@ function enableControls(module) { } let logEl = null; +let persistLog = false; const logBuffer = []; const LOG_MAX_LINES = 1000; +const DEBUG_LOG_KEY = 'xteink-debug-log'; function logLine(message) { logBuffer.push(message); if (logBuffer.length > LOG_MAX_LINES) { logBuffer.splice(0, logBuffer.length - LOG_MAX_LINES); } + if (persistLog) { + sessionStorage.setItem(DEBUG_LOG_KEY, logBuffer.join('\n')); + } if (logEl) { const atBottom = logEl.scrollTop + logEl.clientHeight >= logEl.scrollHeight - 4; logEl.textContent = logBuffer.join('\n'); @@ -171,32 +188,39 @@ function logLine(message) { } } -async function boot(file, variant, setStatus) { +async function boot(file, variant, setStatus, sdCardProvider = loadEphemeralSdCard) { if (!crossOriginIsolated) { throw new Error('This emulator requires COOP/COEP headers. Start it with `make web`.'); } - setStatus('Loading firmware…'); + setStatus('Loading firmware and ephemeral SD card…'); - const firmware = await prepareFirmware(new Uint8Array(await file.arrayBuffer()), import.meta.url); const artifactRoot = new URL('../dist/wasm/', import.meta.url); const qemuUrl = new URL('qemu-system-riscv32.js', artifactRoot); - const rom = new Uint8Array(await fetch(new URL('esp32c3-rom.bin', artifactRoot)).then(response => { - if (!response.ok) { - throw new Error(`Could not load ESP32-C3 ROM (${response.status}).`); - } - return response.arrayBuffer(); - })); + const [firmware, rom, sdCard] = await Promise.all([ + prepareFirmware(new Uint8Array(await file.arrayBuffer()), import.meta.url), + fetchBin(new URL('esp32c3-rom.bin', artifactRoot)), + sdCardProvider(import.meta.url), + ]); + const sdMessage = 'Using a blank ephemeral SD card; writes will be discarded when the emulator restarts.'; + console.log(sdMessage); + logLine(sdMessage); const { default: createQemu } = await import(qemuUrl); + const qemuLogFlags = new URL(location.href).searchParams.has('trace') + ? 'guest_errors,unimp,in_asm' + : 'guest_errors,unimp'; const options = { arguments: [ '-machine', `xteink,variant=${variant}`, + '-accel', 'tcg,tb-size=16', + '-icount', 'shift=auto,align=off,sleep=off', '-L', '/bios', '-display', 'none', '-serial', 'stdio', '-nic', 'none', - '-d', 'guest_errors,unimp', + '-d', qemuLogFlags, '-drive', 'file=/flash.bin,if=mtd,format=raw', + '-drive', 'file=/sd.img,if=sd,format=raw', ], locateFile: path => new URL(path, artifactRoot).href, mainScriptUrlOrBlob: qemuUrl.href, @@ -208,6 +232,7 @@ async function boot(file, variant, setStatus) { options.FS.mkdir('/bios'); options.FS.writeFile('/bios/esp32c3-rom.bin', rom); options.FS.writeFile('/flash.bin', firmware); + options.FS.writeFile('/sd.img', sdCard); }]; setStatus('Starting emulator…'); @@ -222,8 +247,15 @@ function initialize() { const status = document.querySelector('#status'); const canvas = document.querySelector('#screen'); logEl = document.querySelector('#log'); + const params = new URL(location.href).searchParams; + persistLog = params.has('firmware'); + if (persistLog) { + logBuffer.push(...(sessionStorage.getItem(DEBUG_LOG_KEY) || '').split('\n').filter(Boolean)); + logEl.textContent = logBuffer.join('\n'); + } document.querySelector('#log-clear').addEventListener('click', () => { logBuffer.length = 0; + sessionStorage.removeItem(DEBUG_LOG_KEY); logEl.textContent = ''; }); @@ -232,19 +264,15 @@ function initialize() { status.classList.toggle('error', error); }; - form.addEventListener('submit', async event => { - event.preventDefault(); - const file = firmwareInput.files[0]; - if (!file) { - setStatus('Choose a firmware image first.', true); - return; - } - + const start = async (file, variant = variantInput.value) => { bootButton.disabled = true; firmwareInput.disabled = true; variantInput.disabled = true; try { - const module = await boot(file, variantInput.value, setStatus); + const module = await boot(file, variant, setStatus); + if (persistLog) { + window.__xteinkModule = module; + } startDisplayLoop(module, canvas, setStatus, () => enableControls(module)); } catch (error) { console.error(error); @@ -253,7 +281,30 @@ function initialize() { firmwareInput.disabled = false; variantInput.disabled = false; } + }; + + form.addEventListener('submit', event => { + event.preventDefault(); + const file = firmwareInput.files[0]; + if (!file) { + setStatus('Choose a firmware image first.', true); + return; + } + start(file); }); + + const firmwareUrl = params.get('firmware'); + if (firmwareUrl) { + fetch(firmwareUrl) + .then(response => { + if (!response.ok) { + throw new Error(`Could not load debug firmware (${response.status}).`); + } + return response.blob(); + }) + .then(blob => start(new File([blob], 'firmware.bin'), params.get('variant') || 'x3')) + .catch(error => setStatus(error.message || String(error), true)); + } } if (typeof document !== 'undefined') { diff --git a/web/index.html b/web/index.html index 50cd15b..0522a3e 100644 --- a/web/index.html +++ b/web/index.html @@ -31,21 +31,21 @@

Waiting for firmware.

+
-
- -
- -
- -
- - - - - +
+ + +
+ +
+ +
+ + + +
-
@@ -56,6 +56,7 @@

     
+    
   
 
   
diff --git a/web/styles.css b/web/styles.css
index 24dd0be..2a3bc9a 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -43,34 +43,35 @@ button.active { transform: translateY(2px); background: #d2752b; }
 .status { min-height: 1.5rem; margin: 1rem 0; }
 .status.error { color: #a52d20; font-weight: 700; }
 
-.device {
+.workspace { display: grid; grid-template-columns: 1fr 1fr; gap: clamp(1.5rem, 4vw, 3rem); align-items: start; }
+.device { display: flex; justify-content: center; }
+
+.device-frame {
   display: grid;
-  grid-template-columns: minmax(18rem, 33rem) minmax(14rem, 20rem);
-  gap: clamp(1.5rem, 5vw, 4rem);
-  align-items: center;
+  grid-template-columns: auto minmax(14rem, 30rem) auto;
+  grid-template-rows: auto auto auto;
+  gap: .6rem;
+  align-items: stretch;
+  justify-items: stretch;
 }
 
-.screen-shell { padding: 1.1rem; border-radius: 1.2rem; background: #30312e; box-shadow: 0 1rem 2.5rem #5d584c40; }
+.screen-shell { grid-column: 2; grid-row: 2; padding: 1.1rem; border-radius: 1.2rem; background: #30312e; box-shadow: 0 1rem 2.5rem #5d584c40; }
 canvas { display: block; width: 100%; height: auto; background: #fff; image-rendering: pixelated; }
 
-.controls { display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; align-items: center; }
-.controls > button { min-height: 4rem; }
-.power { background: #8e3228; }
-.dpad { grid-column: 1 / -1; display: grid; grid-template: repeat(3, 4.25rem) / repeat(3, 4.25rem); justify-content: center; }
-.dpad button { border-radius: 50%; padding: .25rem; }
-.up { grid-area: 1 / 2; }
-.left { grid-area: 2 / 1; }
-.confirm { grid-area: 2 / 2; }
-.right { grid-area: 2 / 3; }
-.down { grid-area: 3 / 2; }
+.device-frame button { min-height: 0; border-radius: .6rem; }
+.btn-power { grid-column: 2; grid-row: 1; height: 1.6rem; }
+.btn-side { grid-row: 2; width: 1.6rem; }
+.btn-up { grid-column: 1; }
+.btn-down { grid-column: 3; }
+.btn-bottom { grid-column: 2; grid-row: 3; display: grid; grid-template-columns: repeat(4, 1fr); gap: .6rem; }
+.btn-bottom button { height: 1.6rem; }
 
 @media (max-width: 48rem) {
   main { padding-top: 1.5rem; }
-  .device { grid-template-columns: 1fr; }
-  .controls { max-width: 24rem; width: 100%; margin: 0 auto; }
+  .workspace { grid-template-columns: 1fr; }
 }
 
-.log-panel { margin-top: 2rem; }
+.log-panel { min-width: 0; }
 .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; }