Skip to content

piotrgolawski/homm3-browser

Repository files navigation

HoMM3 HotA in the Browser — Docker + Wine + noVNC

Heroes of Might and Magic III: Horn of the Abyss with HD mod, running in Docker and accessible via browser (noVNC) with audio and automatic scaling to browser window size.

Required Game Files

You must provide your own legally obtained copy of the game. This repository contains only the Docker infrastructure — no game files are included.

Required: Heroes of Might and Magic III Complete (Shadow of Death) + Horn of the Abyss (HotA) with HD mod

Tested with:

  • HotA version: 1.8.0
  • HD mod version: 5.6 R30
  • Game base: HoMM3 Complete (GOG edition)

The game directory should contain a fully installed HoMM3 + HotA + HD mod. You can install HotA from heroescommunity.com or h3hota.com.

Access

  • Video: http://localhost:6080
  • Audio: "Click for sound" button in bottom-right corner (required due to browser autoplay policies)
  • Audio port: 8081 (raw PCM stream over HTTP)
  • Remote (Cloudflare): https://homm3.pg7812.org (video), https://homm3-audio.pg7812.org (audio)

Architecture

┌─────────────────────────────────────────────────────────┐
│  Docker container (homm3:latest, network_mode: host)    │
│                                                         │
│  Xtigervnc :99 ──► noVNC proxy ──► port 6080 (HTTP)    │
│       │            (websockify)                         │
│       ▼                                                 │
│  fluxbox (window manager)                               │
│       │                                                 │
│  Wine (win32) ──► HD_Launcher.exe ──► h3hota HD.exe     │
│       │                                                 │
│  PulseAudio ──► null sink ──► parec ──► HTTP :8081      │
│  (system mode)   (virtual)    (per-request PCM stream)  │
│                                                         │
│  Browser: noVNC (video) + Web Audio API (raw PCM audio) │
└─────────────────────────────────────────────────────────┘

Tech Stack

Component Technology Why
Container Docker + docker-compose Isolation, easy restart
Wine wine-stable (win32) Running .exe on Linux
X server + VNC TigerVNC (Xtigervnc) Built-in X server + VNC in one process
Web client noVNC HTML5 VNC client in the browser
Window manager fluxbox Lightweight WM for Wine windows
Audio capture PulseAudio + null sink Virtual audio output without a sound card
Audio streaming Python HTTP + parec (raw PCM) Minimal latency (~100ms)
Audio playback Web Audio API Direct buffer control in the browser
OpenGL Mesa llvmpipe (software) OpenGL 4.5 without GPU
Process manager supervisord Managing all processes inside the container

Configuration Files

File Role
docker-compose.yml Container definition, volumes, network_mode: host
supervisord.conf Processes: TigerVNC, fluxbox, PulseAudio, noVNC, audio stream, game
start_game.sh Waits for X server and PulseAudio, configures Wine audio, launches HD Launcher
stream_audio.sh Waits for PulseAudio and null sink, launches audio_server.py
audio_server.py HTTP server on port 8081, spawns parec per request, streams raw PCM
novnc_index.html Custom page: noVNC + Web Audio API player + auto-scale + auto-reconnect
game/_HD3_Data/Settings/hota.ini HD mod and game settings
game/_HD3_Data/HD_Launcher.ini HD Launcher settings

Graphics Settings (Final)

  • Mode: 32-bit OpenGL (resizable, by Verok) — mode 7
  • Source resolution: 800x600 (original HoMM3)
  • TigerVNC: 1024x640, 24-bit depth (lower resolution = less data over Cloudflare)
  • OpenGL.Upscaling: 5 (SmoothSaI x2) — upscales 800x600 → 1600x1200
  • OpenGL.Interpolation: 5 (Linear Sharper) — sharp image when scaling to window
  • OpenGL.VSync: 0 (VSync doesn't work on llvmpipe + TigerVNC, causes stuttering)
  • MaximizedMode: 1 (game fills entire VNC screen)
  • Multithreading: autodetect (-1)
  • noVNC: scaleViewport=true, qualityLevel=4, compressionLevel=6 (optimized for Cloudflare tunnel)
  • CSS: image-rendering: high-quality + contrast(1.03) + saturate(1.05) — free sharpening on client GPU
  • CPU: ~200% (2 of 12 cores, i5-10600T) — comfortable load

Key to Performance: Lower Source Resolution + SmoothSaI x2

SmoothSaI x2 doubles the pixel count. At source 1380x846, it created 2760x1692 (1.16M pixels through a shader) = 500% CPU. Lowering source to 800x600 means SmoothSaI processes only 480K pixels → 1600x1200 output, scaled to 1024x640 TigerVNC = 200% CPU. Comparable image quality because SmoothSaI operates at its optimal ratio (exactly x2).

Available Verok Filters (from HD_VGL2.dll)

Upscaling: 0=None, 1=xBRZ x2, 2=xBRZ x3, 3=xBRZ x4, 4=CrispSaI x2, 5=SmoothSaI x2

Interpolation: 0=None, 1=Linear, 2=Hermite, 3=Cubic, 4=Lanczos, 5=Linear Sharper, 6=FSR Sharper

Note: SmoothSaI x2 + Linear Sharper gives the best image quality but eats ~500% CPU through llvmpipe (software OpenGL) at high source resolutions. Without upscaling, ~130% CPU.

Management

# Restart container
docker restart homm3

# Game logs
docker exec homm3 tail -50 /var/log/homm3.log

# Supervisord logs
docker exec homm3 tail -50 /var/log/supervisord.log

# Process status
docker exec homm3 ps aux --sort=-%cpu | head -10

# Resource usage
docker stats homm3 --no-stream

# Diagnostic screenshot
docker exec homm3 bash -c 'DISPLAY=:99 import -window root /tmp/screen.png'
docker cp homm3:/tmp/screen.png /tmp/screen.png

# PulseAudio status
docker exec homm3 bash -c 'PULSE_SERVER=unix:/var/run/pulse/native pactl list sink-inputs short'

# Commit container changes (after apt-get install)
docker commit homm3 homm3:latest

# Full rebuild
docker compose up -d --force-recreate

Volumes and Persistence

  • homm3-saves (Docker named volume) → /root/.wine — Wine prefix with all settings and game saves. Survives docker compose down and --force-recreate.
  • /home/p/homm3/game (bind mount) → /game — game files, HD mod configuration.

Game saves: /root/.wine/drive_c/ (inside Wine prefix) or /game/Games/ (symlink).


Problems Encountered and Solutions

1. Docker DNS Resolution — Cannot Pull Ubuntu Image

Problem: dial tcp: lookup registry-1.docker.io: server misbehaving Docker couldn't resolve DNS names. Changing /etc/resolv.conf, daemon.json with DNS 8.8.8.8, restarting systemd-resolved — nothing helped. At one point, hostname resolution also broke (sudo: unable to resolve host p-dell).

Dead ends:

  • Manual /etc/resolv.conf edit — overwritten by systemd-resolved
  • daemon.json with DNS — Docker ignored it in embedded DNS mode
  • Disabling systemd-resolved — broke the entire system

Solution:

  • Added 127.0.0.1 p-dell to /etc/hosts (fixed sudo)
  • Restarted systemd-resolved
  • Manually pulled Ubuntu image (docker pull ubuntu:22.04)
  • apt-get -o Acquire::ForceIPv4=true inside container (IPv6 didn't work)

2. Missing Docker Buildx

Problem: docker-buildx was not installed, Docker Compose couldn't build.

Solution: sudo apt-get install -y docker-buildx

3. DNS Inside Container

Problem: Wine couldn't download updates, wget inside container returned DNS FAIL.

Solution: network_mode: host in docker-compose — container shares host networking.

4. noVNC Download Fail (GitHub)

Problem: wget -qO- downloading noVNC/websockify from GitHub returned a corrupted file (gzip: stdin: unexpected end of file).

Solution: Replaced wget -qO- with wget -q -O saving to a file first, then tar xz. GitHub redirects caused issues with piping.

5. Game Froze on "New Game"

Problem: After clicking "New Game", the game froze — window became unresponsive.

Cause: Discord Game SDK — the game tried to connect to Discord via IPC, causing an infinite loop inside the container without Discord.

Solution: Created an empty discord_game_sdk_x86.dll and renamed discord.dll.

6. HotA Update Loop

Problem: Game displayed an "Update available" dialog and tried to update. Clicking OK caused a crash (no file access), canceling hung the game.

Dead ends:

  • Blocking DNS to update servers — didn't help, game still showed the dialog

Solution: <Update.CheckAtStart> = 0 in hota.ini + disabled in HotA_Setup.ini.

7. xdd.dll Crash (c0000409)

Problem: Game crashed with Unhandled exception code c0000409 (stack buffer overrun) immediately after launch.

Cause: xdd.dll — incompatible DirectDraw module.

Solution: Renamed to xdd.dll.bak2.

8. Case-Sensitivity — Game Files Invisible

Problem: Wine/game looked for DATA/Box64x30.pcx but the folder was named Data (Linux is case-sensitive). Logs: CreateFileW Unable to create file L"Z:\\game\\DATA\\Box64x30.pcx".

Dead ends:

  • Changing permissions (chmod 777) — didn't help because the problem was the name, not permissions
  • This was a long-diagnosed issue because the error said "Unable to create file" which suggested a permissions problem

Solution: Symlinks: DATA → Data, MAPS → Maps, MP3 → Mp3, GAMES → Games.

9. DDrawCompat Crash (DDRAW.dll EXCEPTION_ACCESS_VIOLATION)

Problem: Crash in DDRAW.dll when loading a map — DDrawCompat didn't work with llvmpipe.

Solution:

  • Renamed DDrawCompat.ini to DDrawCompat.ini.disabled
  • WINEDLLOVERRIDES="ddraw=b" — forced Wine's built-in ddraw

10. Missing OpenGL (libGL.so.1 not found)

Problem: Failed to load libGL.so.1 — missing 32-bit OpenGL libraries.

Solution: Installed inside container:

apt-get -o Acquire::ForceIPv4=true install -y \
    libgl1-mesa-glx:i386 libgl1-mesa-dri:i386 libegl1-mesa:i386

Then docker commit homm3 homm3:latest to preserve changes.

11. Xvfb + x11vnc — Display Not Updating in noVNC

Problem: Game was running (confirmed by screenshot with import), but noVNC showed a frozen/gray screen. After refreshing the page, a single frame was visible.

Cause: x11vnc couldn't properly capture changes from Xvfb — polling didn't catch framebuffer updates generated by Wine/OpenGL.

Dead ends:

  • -usepoll -noxdamage for x11vnc — didn't help
  • Changing Xvfb to 16-bit/24-bit — didn't help
  • Different HD mod graphics modes — didn't help

Solution: Replaced Xvfb + x11vnc with TigerVNC (Xtigervnc) — built-in X server with native VNC. One process instead of two, framebuffer = VNC output directly.

12. Game Crash After One Hero Move

Problem: After loading a map and making one move, the game crashed. Reproducible every time.

Partial solution: Combination of many fixes (DDrawCompat, xdd.dll, OpenGL libs, TigerVNC instead of Xvfb). The problem mainly occurred with 16-bit Xvfb + DirectDraw modes. Switching to TigerVNC 24-bit + OpenGL mode 7 (by Verok) stabilized the game.

13. HD Launcher Overwrote hota.ini Settings

Problem: Manually edited values in hota.ini were overwritten after launch by HD_Launcher.exe.

Solution: Understanding that HD_Launcher.ini and the launcher itself manage settings. Final solution: launching the launcher (instead of the game directly) — the user can change settings from the GUI.

14. Audio — ALSA "Access denied"

Problem: Wine couldn't initialize audio: Unknown PCM default, Access denied. Docker container has no physical sound card.

Dead ends:

  • Completely disabling audio (Audio="") — game worked but without sound
  • PulseAudio in user mode — didn't work as root in the container

Solution:

  • PulseAudio in system mode (--system)
  • module-null-sink — virtual audio output (no hardware required)
  • Wine driver pulse + PULSE_SERVER=unix:/var/run/pulse/native
  • Root added to pulse-access group

15. Audio Streaming — ffmpeg -listen Crash

Problem: First version of audio streaming (parec | ffmpeg -listen 1 http://...) crashed regularly (exit status 1).

Cause: ffmpeg -listen is unstable, supports one client at a time, crashes after disconnection.

Solution: Python HTTP server (audio_server.py) with ThreadingTCPServer. Each GET request spawns a dedicated parec pipeline. Stable, supports multiple clients.

16. Audio Latency — 3 Seconds of Delay

Problem: First version with HTML <audio> element + MP3 encoding had ~3s latency.

Cause:

  • MP3 encoding latency (~100ms)
  • Browser <audio> element pre-buffers aggressively (~2-3s)

Solution: Complete architecture change:

  • Removed ffmpeg — stream raw PCM (s16le, 44100Hz, stereo) directly from parec
  • Instead of <audio> element → Web Audio API with manual scheduling
  • 40ms chunks scheduled 80ms ahead
  • Final latency: ~100ms

17. SmoothSaI x2 — 500% CPU

Problem: Upscaling filter SmoothSaI x2 in OpenGL mode consumed ~500% CPU.

Cause: GPU shader (GLSL) executed on CPU through llvmpipe (software renderer). Each frame: 1.16M pixels × complex SmoothSaI algorithm = brutal for CPU.

Solution: Lowered source resolution from 1380x846 to 800x600 with SmoothSaI x2 enabled. Filter processes 480K pixels instead of 1.16M = CPU dropped from 500% → 200%. Comparable quality because SmoothSaI x2 upscales 800x600 → 1600x1200, which is then scaled to 1024x640 TigerVNC. Linear Sharper (5) as interpolation — lighter than FSR Sharper, doesn't cause stuttering on llvmpipe.

18. Launcher Auto-Restart

Problem: If the user clicks "Exit" in HD Launcher, the process ends and there's no way to return to the game without restarting the container.

Solution: autorestart=true in supervisord for the homm3 process. Launcher restarts automatically after closing (~15-20s, waiting for X server and PulseAudio). Also works after game crash — after returning to the launcher, if it gets closed, it respawns on its own.

19. Image Quality Without Excessive CPU Cost

Problem: SmoothSaI x2 gave excellent quality but 500% CPU at 1380x846.

Dead ends:

  • FSR Sharper (Interpolation=6) — caused stuttering on llvmpipe
  • VSync=1 — froze the renderer (no real refresh rate on TigerVNC)
  • HigherFPS=2 — more frames = more llvmpipe work = stuttering
  • qualityLevel=9 in noVNC — larger VNC frames = stuttering during transmission

Solution: Lowered source resolution to 800x600 + SmoothSaI x2. Filter processes 4x fewer pixels. Additional free improvements on the client side:

  • CSS image-rendering: high-quality — better scaling on browser GPU
  • CSS filter: contrast(1.03) saturate(1.05) — sharpening + more vivid colors
  • noVNC qualityLevel=4 — smoother transfer, CSS compensates for quality

20. Cloudflare Tunnel — Smoothness vs Quality

Problem: Through Cloudflare tunnel, the game stuttered — too much data per VNC frame.

Solution: Trading quality for smoothness:

  • TigerVNC 1380x846 → 1024x640 (44% fewer pixels to transmit)
  • compressionLevel 2 → 6 (stronger compression, more server CPU but less bandwidth)
  • qualityLevel 6 → 4 (stronger JPEG, CSS contrast/saturate compensates for sharpness)
  • Audio on a separate subdomain (homm3-audio.pg7812.org) because Cloudflare tunnel = one port per hostname

Key principle: over the internet, trade CPU for bandwidth — we have CPU to spare (200%/1200%), bandwidth is the bottleneck.

Compromises / Things That Couldn't Be Fully Solved

  1. SmoothSaI x2 at high resolution — technically works, but too much CPU load through software rendering. Would work great on a PC with a real GPU.

  2. Full audio quality — raw PCM stream uses ~1.4 Mbps bandwidth (OK for LAN, may be an issue over internet). Alternative: Opus encoding with lower latency than MP3, but would require rewriting the client.

  3. Cloudflare Tunnel audio — configured on separate subdomain (homm3-audio.pg7812.org). Works but requires two DNS entries per deployment.

  4. Auto-save persistence edge case — if someone runs docker compose down -v, the named volume homm3-saves will be deleted along with Wine prefix and saves. Safe options: docker compose down (without -v), docker restart, --force-recreate.

Reproducing From Scratch

# 1. Requirements
sudo apt-get install docker.io docker-compose-v2 docker-buildx unzip

# 2. Unpack game files (you must provide your own copy)
mkdir -p ~/homm3/game
unzip "/path/to/HoMM 3 Hota + maps installed.zip" -d ~/homm3/game

# 3. Create symlinks (case-sensitivity fix)
cd ~/homm3/game
ln -sf Data DATA; ln -sf Maps MAPS; ln -sf Mp3 MP3; ln -sf Games GAMES

# 4. Disable problematic DLLs
mv xdd.dll xdd.dll.disabled
mv DDrawCompat.ini DDrawCompat.ini.disabled

# 5. Disable Discord DLL
mv discord.dll discord.dll.disabled

# 6. Build image (Dockerfile with Wine, TigerVNC, noVNC, PulseAudio, ffmpeg, mesa, etc.)
# The image is already built and committed as homm3:latest

# 7. Run
cd ~/homm3
docker compose up -d

# 8. Open http://localhost:6080

Packages Installed in Container (docker commit)

Beyond what was in the Dockerfile, the following were manually installed:

  • libgl1-mesa-glx:i386 libgl1-mesa-dri:i386 libegl1-mesa:i386 — 32-bit OpenGL
  • tigervnc-standalone-server — TigerVNC (replaced Xvfb + x11vnc)
  • pulseaudio — audio in system mode
  • ffmpeg — (optional, unused in final audio version)
  • mesa-utils — glxinfo for diagnostics
  • binutils — strings for DLL analysis
  • imagemagick — import for diagnostic screenshots

About

HoMM3 HotA in the browser — Docker + Wine + noVNC with audio

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors