Skip to content

fix(int33): restore original DOSBox mickey formula in getRelMickey - #24

Open
abedegno wants to merge 3 commits into
caiiiycuk:8.xxfrom
abedegno:fix/mickey-rate-int33h-fn0b
Open

fix(int33): restore original DOSBox mickey formula in getRelMickey#24
abedegno wants to merge 3 commits into
caiiiycuk:8.xxfrom
abedegno:fix/mickey-rate-int33h-fn0b

Conversation

@abedegno

Copy link
Copy Markdown

Summary

getRelMickey in native/jsdos/jsdos-mouse.cpp has two factors that don't appear in upstream DOSBox and that together halve the mickey stream delivered to DOS programs via INT 33h fn 0x0B (Read Motion Data) and the INT 74 mouse callback:

auto pxPerCol = surfaceWidth / (float) (mouse.max_x - mouse.min_x);   // ≈ 0.5 for VGA mode 13h
int mickey_x = round(dCol * pxPerCol * mouse.mickeysPerPixel_x);
int mickey_y = round(dRow * pxPerRow * mouse.mickeysPerPixel_y / 2);  // ``// why div 2?``

Upstream DOSBox uses the simpler mickey += delta * mickeysPerPixel. The pxPerCol factor is ≈ 0.5 for standard VGA mode 13h because INT 33h reports X over a doubled 0..639 range while the rendered screen is 320 pixels wide — so every mickey we report is half what upstream DOSBox would report. The /2 on Y is flagged as suspect by the very comment next to it.

Any DOS game that relies on the mickey stream (most mouse-aware games from the Microsoft-mouse-driver era) therefore sees cursor motion at roughly ½ the expected rate in js-dos, while the same game run on native DOSBox tracks 1:1.

Repro

Ultima Underworld I/II is a clean case. UW.EXE reads cursor motion exclusively through fn 0x0B, never fn 0x03 (confirmed by disassembling the three int 33h sites in the binary):

  • 0x22c68 — init: mov ax,0; int 33h, then stores 200 into [0x360] and [0x362].
  • 0x22c87 — tick: mov ax,0Bh; int 33h, then calls the helper at 0x22c98 on both mickey_x (from CX) and mickey_y (from DX).
  • 0x22ca3 — button read only: mov ax,3; int 33h; and ax, 3; ret (POS_X / POS_Y are discarded).

Helper at 0x22c98 is:

mov bx, 64h        ; BX = 100
imul bx            ; DX:AX = mickey * 100
idiv [0x360]       ; AX   = mickey * 100 / 200  =  mickey / 2
xchg bx, ax
ret

UW1's cursor-per-pixel rate is therefore exactly mickey / 2. With the current js-dos rate (half-mickey per dCol), UW1's crosshair travels ≈ ½ the width of the viewport for a full host-mouse drag across the canvas. With the formula restored to upstream, it tracks 1:1 — verified by driving UW1 inside a local build.

Fix

Drop the pxPerCol/pxPerRow and /2 factors. Use the original DOSBox formula, applied directly to the absolute col/row deltas, and invert it the same way for rel.col/rel.row. Also carry a per-axis fractional residual across calls so sub-mickey motions (e.g. dCol = 0.5 from a slow incremental pass-through) don't round to zero and leave mouse.mickeyCol stuck behind mouse.col — which was causing a secondary bug where slow drags stutter or stall.

Compatibility note

This doubles the mickey rate delivered by fn 0x0B / INT 74 vs previous js-dos releases. If anyone had calibrated their integration around the halved rate, their game will now feel twice as fast. The new rate matches native DOSBox, so any game that works on native DOSBox will work here. But if you'd prefer to gate this behind a flag rather than change default behavior, happy to reshape it that way.

Paths not affected:

  • fn 0x03 (absolute position) — reads mouse.col / mouse.row directly, unchanged.
  • Pointer-locked / relative mode — separate code path in server_mouse_moved, unchanged.
  • Games that set their own mickey rate via fn 0x0F — mickeysPerPixel_x/y is still multiplied in, so game-set rates still apply.

Related issues

Plausibly the root cause (or a contributor) for:

  • #316 — "Mouse works very slowly in WarCraft II"
  • #351 — "mouse cursor doesn't move on all of the screen (jsdos v8 bug)"
  • #313 — "inaccurate mouse positioning" in DOS games

abedegno and others added 3 commits April 24, 2026 15:08
Symptom: DOS games that track the cursor via INT 33h fn 0x0B (Read Motion
Data) or the INT 74 mouse callback see cursor motion at roughly half the
expected rate. In Ultima Underworld 1/2 the main-menu crosshair travels
about half the width/height of the viewport for a full edge-to-edge host
mouse drag. Same engine on native DOS/DOSBox tracks 1:1.

Root cause: getRelMickey in native/jsdos/jsdos-mouse.cpp diverged from the
original DOSBox formula by introducing two extra factors:

  pxPerCol = surfaceWidth / (max_x - min_x)    ≈ 0.5 for VGA mode 13h
                                                 (INT 33h reports X in a
                                                 doubled 0..639 range over
                                                 a 320-pixel screen)
  <div by 2> on mickey_y                         // why div 2?

Together they halve the mickey count that fn 0x0B returns. UW1/UW2
(disassembled UW.EXE at 0x22c98 shows the mickey reader as
'imul bx, 100; idiv [0x360]=200' → mickey/2) inherit that loss and
render the cursor at half speed on both axes.

Fix: drop the two extra factors and use the original DOSBox formula,
'mickey = delta * mickeysPerPixel', applied directly to the absolute
col/row delta. Invert it the same way to compute rel.col / rel.row.

Also carry a per-axis fractional residual across getRelMickey calls so
that sub-mickey motions (e.g. dCol = 0.5 spread over many cumulative
pass-throughs) don't silently round to zero. Without this, slow drags
stutter or stall when the computed mickey delta rounds to 0 and rel.col
never advances past the previous mickeyCol checkpoint.

Verified against UW1 inside the js-dos-emu build: cursor now tracks 1:1
with the host mouse across the full canvas on both X and Y, matching
native DOSBox behavior.
The getRelMickey sync pass zeroed mouse.col and mouse.row while the
mickeyRelSyncTries counter was non-zero. Because every wc-mouse-sync
message re-arms that counter to 3, any bridge that calls
ci.sendMouseSync() after each event (e.g. abedegno/dos-mcp)
keeps the reset loop permanently active — so INT 33h fn 0x03
reports POS_X=POS_Y=0 forever, even while Mouse_CursorMoved is
landing correct absolute coords in mouse.col/mouse.row.

The intent of the sync (per caiiiycuk's comment) is to clear
accumulated RELATIVE mickey drift on focus-regain, not to
teleport the cursor to (0,0). Drop the mouse.col/row zeroing and
keep the mickeyCol/mickeyRow reset; return the current col/row
in the mickey struct so callers downstream of getRelMickey see
the true absolute position.
The prior patch kept mouse.col/mouse.row untouched through the
getRelMickey sync-retry loop so INT 33h fn 0x03 would read the
correct absolute position, but it still returned a sentinel
-(max - min) mickey delta each pass. Games that derive cursor
position from fn 0x0B / INT 74 callbacks (Ultima Underworld being
the canonical case) accumulate that delta and walk the in-game
cursor off-screen fast — one big negative per 55 ms poll.

A sync is meant to flush the internal mickey accumulator, not
teleport the consumer. Return zero mickey delta plus current
col/row so fn 0x0B consumers stay in lockstep with fn 0x03
consumers during the sync window.
@caiiiycuk

Copy link
Copy Markdown
Owner

Hi, nice. Did you also compare this implementation to dosbox-x?

@abedegno

abedegno commented Jun 5, 2026

Copy link
Copy Markdown
Author

@caiiiycuk Thanks! I've gone and compared against dosbox-x's INT 33h path (src/ints/mouse.cpp, Mouse_CursorMoved + Mouse_Read_Motion_Data).

Short version: the two substantive parts of this change line up with dosbox-x, and there are two known deltas I want to be upfront about rather than claim a perfect match.

Where this matches dosbox-x:

  1. Fractional mickey accumulation. The mickey_x_residual / mickey_y_residual carry I added is essentially the same idea as dosbox-x's mickey_accum_x/y:

    mouse.mickey_accum_x += (dx * mouse.mickeysPerPixel_x);
    if (fabs(mouse.mickey_accum_x) >= mickey_threshold) {
        mouse.mickey_x += truncf(mouse.mickey_accum_x);
        mouse.mickey_accum_x -= truncf(mouse.mickey_accum_x);
    }

    They accumulate the fractional part and only flush the integer mickeys, which is what stops slow drags from rounding away to zero. I landed on the same approach independently (mine uses round + residual rather than truncf + threshold, but same intent).

  2. No Y /2 on the DOS-driver path. The old // why div 2? halving looks like it was compensating for the PS/2 path. In dosbox-x the dy *= 2 is gated on useps2callback - i.e. it only applies to the PS/2 BIOS-callback path, not the DOS-driver INT 33h path that UW1/UW2 actually use. So removing the unconditional /2 here is consistent with dosbox-x for the path that matters in js-dos.

Two places I differ from dosbox-x:

  1. Sensitivity (fn 0x1A senv). dosbox-x scales motion by senv_x/senv_y before accumulating mickeys; this change doesn't model fn 0x1A sensitivity at all. So a game that adjusts mouse sensitivity through the driver won't be honored the way dosbox-x honors it. I left this out to keep the fix minimal, but happy to add it if you'd prefer parity.

  2. Gain factor. In dosbox-x the pixelPerMickey_x * mickeysPerPixel_x terms cancel to ≈1, so the net per-axis gain is basically senv (≈1 at default). This change applies mickeysPerPixel_x directly, so it matches dosbox-x's net rate only when mickeysPerPixel_x ≈ 1. That holds for the cases I tested (UW1/UW2, plus the dos-mcp bridge), but it's a structural difference worth noting rather than a guaranteed equivalence across all configs.

One more thing not in dosbox-x's scope: the sync-pass change (returning a zero mickey delta + current absolute col/row instead of the old -(max-min) sentinel) is js-dos-specific - it fixes bridges that sync on every event (e.g. dos-mcp) pinning the cursor at (0, 0), and UW walking off-screen from accumulating one big negative delta per poll. It still clears mickeyCol/mickeyRow, so the read-and-clear contract that fn 0x0B relies on is preserved.

If you'd rather I bring this fully in line with dosbox-x - i.e. add senv and make the gain explicitly cancel to 1 - I'm glad to push that. Just wanted to keep the first pass scoped to the UW regression.

@caiiiycuk

Copy link
Copy Markdown
Owner

Just releases jsdos v8.4.0, now I can look into it!

@caiiiycuk

Copy link
Copy Markdown
Owner

@abedegno , please take a look: abedegno#1

@caiiiycuk

Copy link
Copy Markdown
Owner

I also tested it locally, so far looks good, also please rebase

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.

2 participants