Skip to content

bindings/jni: Add NS16550A ring-buffer bridge for JVM serial routing#1

Merged
SolAstrius merged 1 commit into
stagingfrom
feat/uart-jni-bridge
Apr 21, 2026
Merged

bindings/jni: Add NS16550A ring-buffer bridge for JVM serial routing#1
SolAstrius merged 1 commit into
stagingfrom
feat/uart-jni-bridge

Conversation

@SolAstrius
Copy link
Copy Markdown
Collaborator

Summary

  • Adds a second NS16550A UART whose chardev backend is a pair of 64 KiB ring buffers instead of stdio, so Java-side consumers can drive guest serial programmatically (in-game console, host-driven RPC transport, etc).
  • Four new JNI exports (`ns16550a_bridge_init / poll / feed / stats`) and a thin `NS16550ABridge` wrapper class.
  • Zero changes to librvvm core — `ns16550a_init_auto` already accepts a pluggable `chardev_t`; this just adds a factory for embedders.

Architecture

Mirrors the HDA ring pattern from `feat/sound-backend-api`:

  • Bridge state is a `chardev_t` + two `ringbuf_t` + a `spinlock_t`.
  • `poll` / `feed` own the lock; `chardev_notify` is invoked outside the lock after an RX/TX flag edge to avoid re-entering the UART while holding state.
  • TX overflow drops oldest bytes (latency-first, same philosophy as the HDA ring). RX overflow reports a short write; caller retries.
  • Lifetime is owned by the MMIO dev: `ns16550a_remove` → `chardev_free` → our `remove` → `free()`. No explicit JNI teardown call.

Test plan

  • Builds with `USE_JNI=1 USE_LIB=1` on Linux x86_64.
  • All four JNI symbols exported in `librvvm.so`:
    ```
    Java_lekkit_rvvm_RVVMNative_ns16550a_1bridge_1{init,poll,feed,stats}
    ```
  • `javac` passes on `NS16550ABridge.java` + updated `RVVMNative.java`.
  • Integration test with ScalarEvolution (follow-up PR wires this into `RvvmMachineBackend`).

Expose a second NS16550A UART whose chardev backend is a pair of 64 KiB
ring buffers instead of stdio. Java-side consumers drain guest TX via
ns16550a_bridge_poll and inject guest RX via ns16550a_bridge_feed —
enough to implement an in-game serial console or a host-driven RPC
transport on top of a stock NS16550A, without patching the UART or
exposing chardev internals through JNI.

Architecture mirrors feat/sound-backend-api's HDA ring pattern:

 - ns16550a_bridge_init attaches the UART via the existing
   ns16550a_init_auto(machine, chardev) path and returns an opaque
   bridge handle.
 - poll / feed own a spinlock guarding two ringbufs; chardev read/write
   from the UART's thread and the Java thread contend on the same lock.
 - chardev_notify is called outside the lock after an RX/TX flag edge,
   so the UART's IRQ path doesn't re-enter while we hold state.
 - TX overflow drops oldest bytes (HDA's latency-first policy). RX
   overflow reports a short write; caller retries.
 - ns16550a_bridge_stats exposes {pushed, popped, fed, consumed,
   dropped} for instrumentation and tests.

The bridge lifetime is owned by the MMIO device: ns16550a_remove calls
chardev_free on our chardev, which frees the bridge struct and destroys
the ringbufs. No explicit JNI teardown call.

Zero changes to librvvm core — ns16550a.c already accepts a pluggable
chardev (ns16550a_init_auto). The stdio default (ns16550a_init_term_auto)
is unchanged; this just adds a second factory for embedders.
@SolAstrius SolAstrius merged commit 1ad9bf3 into staging Apr 21, 2026
@SolAstrius SolAstrius deleted the feat/uart-jni-bridge branch April 21, 2026 02:34
SolAstrius added a commit that referenced this pull request Apr 27, 2026
Linux's snd_hdac_bus_init_cmd_io (sound/hda/core/controller.c) does:

  writew(CORBRP, AZX_CORBRP_RST);           // bit 15 = 1
  for (timeout = 1000; timeout > 0; timeout--)
      if (readw(CORBRP) & AZX_CORBRP_RST)   // poll for bit 15 = 1
          break;
  // ↑ "CORB reset timeout #1" if poll times out

  writew(CORBRP, 0);
  for (timeout = 1000; timeout > 0; timeout--)
      if (readw(CORBRP) == 0)               // poll for bit 15 = 0
          break;
  // ↑ "CORB reset timeout #2" if poll times out

Our handler reset corb_rp=0 immediately on the bit-15 write but never
echoed bit 15 back on read, so Linux's first poll always timed out
(1000 µs of busy-looping, then the dev_err warning). The cascade from
there causes spurious response timeouts during codec discovery —
Linux falls back to polling mode, retries, and somewhere in that
chaos NID 2's widget caps response gets dropped or returns garbage.
Linux mis-classifies the topology: NID 2 disappears entirely from the
codec dump, and snd_hda_codec_generic configures the pin (NID 3) as
"mono_out=0x3" with no converter underneath. No PCM device is
created, no Master mixer control exists, speaker-test errors with
ENOENT.

Fix: store corb_rprst (bit 15) separately from corb_rp (bits 7:0),
echo on read. SW writes 1 → set rprst, reset corb_rp to 0; SW writes
0 → clear rprst, store new RP value. Both Linux poll loops now exit
on the first iteration.

This regression went unnoticed because all the standalone smoke
boots in this session ran rvvm_x86_64 without the -hda_test flag —
HDA wasn't actually attached to the PCI bus. The user discovered it
when running through the ScalarEvolution mod's full machine config
(which always attaches HDA) and seeing playback fail.

The CORBRP RST handshake has been broken since this device was first
written; before commit 5d10843 (table-driven SD dispatch) the code
also reset corb_rp=0 on bit-15 write without echoing the bit. It
worked for users in practice because Linux only logs the timeout as
a warning and continues — the real damage was the 1 ms × 1000 = 1 s
delay during which subsequent codec verbs raced. Some guests were
lucky and got a clean topology; others (apparently anyone with the
new Beep widget bumping subnode count to 3) tipped over.
SolAstrius added a commit that referenced this pull request May 1, 2026
)

Expose a second NS16550A UART whose chardev backend is a pair of 64 KiB
ring buffers instead of stdio. Java-side consumers drain guest TX via
ns16550a_bridge_poll and inject guest RX via ns16550a_bridge_feed —
enough to implement an in-game serial console or a host-driven RPC
transport on top of a stock NS16550A, without patching the UART or
exposing chardev internals through JNI.

Architecture mirrors feat/sound-backend-api's HDA ring pattern:

 - ns16550a_bridge_init attaches the UART via the existing
   ns16550a_init_auto(machine, chardev) path and returns an opaque
   bridge handle.
 - poll / feed own a spinlock guarding two ringbufs; chardev read/write
   from the UART's thread and the Java thread contend on the same lock.
 - chardev_notify is called outside the lock after an RX/TX flag edge,
   so the UART's IRQ path doesn't re-enter while we hold state.
 - TX overflow drops oldest bytes (HDA's latency-first policy). RX
   overflow reports a short write; caller retries.
 - ns16550a_bridge_stats exposes {pushed, popped, fed, consumed,
   dropped} for instrumentation and tests.

The bridge lifetime is owned by the MMIO device: ns16550a_remove calls
chardev_free on our chardev, which frees the bridge struct and destroys
the ringbufs. No explicit JNI teardown call.

Zero changes to librvvm core — ns16550a.c already accepts a pluggable
chardev (ns16550a_init_auto). The stdio default (ns16550a_init_term_auto)
is unchanged; this just adds a second factory for embedders.
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