Skip to content

Add support for Hexagon architecture#4933

Open
androm3da wants to merge 11 commits into
bytecodealliance:mainfrom
androm3da:hexagon-upstream-v2
Open

Add support for Hexagon architecture#4933
androm3da wants to merge 11 commits into
bytecodealliance:mainfrom
androm3da:hexagon-upstream-v2

Conversation

@androm3da

Copy link
Copy Markdown

No description provided.

@androm3da
androm3da force-pushed the hexagon-upstream-v2 branch 5 times, most recently from 0ccab07 to 4e6cd5a Compare May 7, 2026 19:41
@androm3da
androm3da force-pushed the hexagon-upstream-v2 branch from 4e6cd5a to ac35bf8 Compare June 3, 2026 19:53
androm3da added 11 commits June 27, 2026 20:07
Split V128 memory access (LOAD_V128/STORE_V128) into a separate
three-way guard independent of the scalar unaligned-access flag,
and add a target_supports_unaligned_simd field to AOTCompContext.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
Add interpreter and AOT execution support for Qualcomm Hexagon DSP:
invokeNative trampoline, ELF relocation handler with PLT stubs,
wamrc --target=hexagon with auto -small-data, SIMD enablement, and
cross-compilation platform cmake.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
WASMTableInstance contains 64-bit members (the elem_ref_type union), so
alignof(WASMTableInstance) == 8, and the compiler may legitimately emit
64-bit stores against it (e.g. merging the adjacent 32-bit cur_size and
max_size stores in tables_instantiate). But the table region layout
packed each instance at offsetof(elems) + elem_count * 4 bytes on 32-bit
targets, so any table with an odd element count left the next instance
only 4-byte aligned, and the global data preceding the region could
leave even the first one misaligned. On strict-alignment targets
(Hexagon) instantiating such a module faults with SIGBUS; elsewhere it
is silent undefined behavior.

Round each table instance size and the table region start up to 8 bytes
in the allocation sizing, first_table derivation, and both
tables_instantiate walk loops, which must all stay in sync.

Found running the WASM spec suite (call_indirect, multiple-tables
module) under qemu-hexagon user-mode with unaligned-access faulting
enabled.
Three fast-interp loader paths did raw multi-byte stores to addresses
that are not guaranteed naturally aligned, faulting (SIGBUS) on
strict-alignment targets such as Hexagon:

- The WASM_OP_SELECT / SELECT_128 label fixup wrote the replacement
  label address with a raw *(uint32 *) / *(int32 *) cast into the
  compiled-code stream at p_code_compiled_tmp - 4. The stream packs
  1- and 2-byte items, so that address can be 2-byte aligned. Use the
  alignment-safe STORE_U32 helper, matching wasm_loader_emit_uint32.

- reserve_block_ret and copy_params_to_dynamic_space carve a uint8
  cells[] array and int16 offset arrays out of one allocation with no
  padding between them, so an odd element count leaves the int16
  arrays on odd addresses. Round the cells region up to 2 bytes.

Found running the WASM spec suite (left-to-right, block/br/fac/func/if
and const) under qemu-hexagon user-mode with unaligned-access faulting
enabled.
execute_func marshals wasm arguments into a uint32 cell array, which is
only guaranteed 4-byte aligned. The V128 parameter and result paths cast
&argv1[p] to uint64* and dereferenced it directly, faulting (SIGBUS) on
strict-alignment targets such as Hexagon whenever the V128 lands on a
4-mod-8 cell boundary (e.g. any export taking (i32, v128)). The i64 and
f64 paths already store word-by-word through a union; do the same for
V128 argument parsing and result printing.

Found running the WASM SIMD spec suite (simd_align etc., 21 cases)
under qemu-hexagon user-mode with unaligned-access faulting enabled.
The SIMD_v128_const handler read its 16-byte immediate with
*(V128 *)orig_ip, but frame_ip points into the compiled code stream,
which only guarantees 2-byte alignment. On strict-alignment targets
(Hexagon) the resulting 64-bit loads fault with SIGBUS as soon as a
function containing v128.const executes (e.g. every *_with_const_*
export in the SIMD spec suite). Use LOAD_V128, which is
alignment-safe on such targets, like the v128.load handler already
does; the v8x16.shuffle handler already copies its immediate with
bh_memcpy_s.

Found running the WASM SIMD spec suite (simd_const and the *_arith2
suites) under qemu-hexagon user-mode with unaligned-access faulting
enabled.
…ayout

Commit bc2f45e ("fix(interpreter): keep WASMTableInstance instances
8-byte aligned") padded the table region in wasm_runtime.c (global
data rounded up to 8 bytes, each table instance rounded up to 8 bytes)
so instances stay naturally aligned for their 64-bit members on
strict-alignment targets. But that layout is duplicated in three more
places that were not updated:

- fast-jit computes table offsets statically in
  jit_frontend_get_table_inst_offset()/get_first_table_inst_offset();
  with the runtime layout shifted by the new padding, any module with
  global_data_size % 8 != 0 read table slots at stale offsets.
  Reproduced on x86-64 fast-jit with a module holding one i32 global
  and a 2-entry funcref table: call_indirect raised "indirect call
  type mismatch" for element 0 and called the wrong function for
  element 1.
- LLVM-JIT bakes the same offsets into generated code through
  get_tbl_inst_offset() in aot_emit_table.c, with the same failure
  mode.
- aot_runtime.c still packed AOTTableInstance records with no padding,
  leaving AOT mode exposed to the same misaligned 64-bit accesses on
  strict-alignment targets that motivated the interpreter fix.

Apply the identical alignment rule in all three places, so the four
sites that duplicate this layout (wasm_runtime.c, aot_runtime.c,
aot_emit_table.c, jit_frontend.c) agree again. get_tbl_inst_offset()
computes the padding from the target's pointer size, so cross-compiled
AOT output matches the target runtime's layout. Also fix the
pre-existing sizeof(uint32) elem size in jit_frontend's import-table
walk, which under-counted the stride of imported tables on 64-bit
(elems are table_elem_type_t, i.e. pointer-sized).

Note: generated AOT/JIT code and the runtime must come from the same
tree, as the in-memory instance layout changed; the AOT file format is
unaffected.

Verified: the reproducer now passes under fast-jit, host AOT and
hexagon AOT (qemu user-mode); full spec suite green in aot mode on
hexagon and the layout is a no-op wherever global data size and table
strides were already multiples of 8.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
…ands

Wasm linear-memory addresses can have any alignment -- the memarg
align field is only a hint -- but two interpreter paths dereferenced
them with helpers that require alignment, faulting with SIGBUS on
strict-alignment targets (found on Hexagon under qemu user-mode with
unaligned-access faulting; silent UB elsewhere):

- classic interpreter, plain i64.store/f64.store: wrote through
  PUT_I64_TO_ADDR, which performs two 32-bit stores and is only meant
  for the 4-byte-aligned operand stack. memory.wast ("cast" performs
  i64.store align=1 at address 9), float_memory.wast and
  call_indirect.wast all crashed. Switch to the alignment-safe
  STORE_I64. This was the only remaining unsafe linear-memory access
  in wasm_interp_classic.c (all other maddr uses already go through
  LOAD_*/STORE_*).

- fast interpreter, SIMD lane/zero loads and lane stores:
  SIMD_LOAD_LANE_COMMON (shared by v128.loadN_lane and v128.loadN_zero)
  read 16/32-bit lanes with direct dereferences and 64-bit lanes with
  the stack-only GET_I64_FROM_ADDR; SIMD_STORE_LANE_OP wrote 16/32-bit
  lanes directly. simd_load16/32/64_lane, simd_load_zero and
  simd_store16/32_lane crashed, while simd_store64_lane -- already
  using STORE_I64 -- passed, confirming the diagnosis. Switch to
  LOAD_U16/LOAD_U32/LOAD_I64 and STORE_U16/STORE_U32.

On targets with WASM_CPU_SUPPORTS_UNALIGNED_ADDR_ACCESS the helpers
compile to the same direct accesses as before, so this is a no-op
there (re-verified on x86-64).

With these fixes the memory, call_indirect, float_memory and all nine
simd lane/zero spec suites pass on Hexagon in both interpreters.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
The CLI argument parser trusted the bit pattern strtof/strtod return
for NaN inputs, only patching in the sign for a leading '-' and an
explicit payload after ':'. That breaks on platforms whose libc does
not return the canonical quiet NaN: musl on Hexagon returns the
hardware default NaN 0xffffffff from strtof("nan") -- negative sign,
all-ones payload -- because the parsed double is narrowed by a convert
instruction that canonicalizes NaNs. Any spec assert passing a
positive NaN argument to a sign-sensitive op then failed, e.g.

    (assert_return (invoke "copysign" (f32.const -0x1p-149)
                                      (f32.const nan))
                   (f32.const 0x1p-149))

returned -0x1p-149 because the "nan" argument arrived negative.

An MSVC-only workaround for the same class of problem (strtof
returning a non-canonical payload) already existed; make it
unconditional: on isnan, reset the value to nanf("")/nan("")
(0x7fc00000 / 0x7ff8000000000000) before applying the requested sign
and payload. On glibc this is a behavioral no-op since strtof("nan")
already returns exactly that value.

Fixes the f32_bitwise and conversions spec suites on Hexagon; they are
removed from the skip list in the follow-up test-script commit.

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
The spec-test workflow built a single iwasm (the product-mini defaults:
fast-interp with SIMD) and ran it for every matrix cell, so the
classic-interp cells never actually exercised the classic interpreter
-- and SIMD + classic-interp is not a supported build combination in
the first place. Build two artifacts instead (fast-interp+SIMD, and
classic-interp with -DWAMR_BUILD_FAST_INTERP=0 -DWAMR_BUILD_SIMD=0),
download the right one per running mode, and exclude the
SIMD x classic-interp cell with a comment. The aot cell keeps the
fast-interp binary; both builds have AOT support and the interpreter
kind does not matter there.

Update the hexagon skip list in all.py to match verified reality
(fast-interp, classic-interp and aot all green under qemu-hexagon
user-mode with these skips):

- drop f32_bitwise and conversions: they pass after the NaN
  CLI-argument normalization in wasm_application.c.
- float_exprs: keep, now attributed correctly -- Hexagon FP arithmetic
  produces the default NaN (all-ones payload) rather than the
  wasm-canonical quiet NaN, so the *_nan_bitpattern checks fail (same
  class as the x87 skip for i386).
- simd pmin/pmax: keep, now attributed correctly -- clang's Hexagon
  backend lowers the strict-IEEE "(b < a) ? b : a" pattern to
  sfmin/sfmax (minNum NaN semantics) without nnan, so lanes with a NaN
  first operand come back wrong.
- simd_lane / simd_splat: keep -- musl (LD64) strtod parses long
  hex-float CLI arguments 1 ulp low (e.g. 0x0123456789ABCDEFabcdef); a
  test-harness artifact, not an engine bug.
- i32 / i64: keep skipped for the clang rotl/rotr miscompile (fixed by
  llvm-project 006429da6ab9); re-enabling needs the fix in BOTH the
  Hexagon cross toolchain that builds iwasm (interp modes pass with a
  fixed 22.1.8 build) AND the LLVM wamrc links (aot fails with any
  unfixed LLVM, observed from 18.1.x through 22.x).

Signed-off-by: Brian Cain <brian.cain@oss.qualcomm.com>
@androm3da
androm3da force-pushed the hexagon-upstream-v2 branch from ac35bf8 to fa8dfe5 Compare July 16, 2026 21:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant