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.
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.
- 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)
┌─────────────────────────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────────────────────────┘
| 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 |
| 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 |
- 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
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).
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.
# 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-recreatehomm3-saves(Docker named volume) →/root/.wine— Wine prefix with all settings and game saves. Survivesdocker compose downand--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).
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.confedit — overwritten by systemd-resolved daemon.jsonwith DNS — Docker ignored it in embedded DNS mode- Disabling systemd-resolved — broke the entire system
Solution:
- Added
127.0.0.1 p-dellto/etc/hosts(fixed sudo) - Restarted
systemd-resolved - Manually pulled Ubuntu image (
docker pull ubuntu:22.04) apt-get -o Acquire::ForceIPv4=trueinside container (IPv6 didn't work)
Problem: docker-buildx was not installed, Docker Compose couldn't build.
Solution: sudo apt-get install -y docker-buildx
Problem: Wine couldn't download updates, wget inside container returned DNS FAIL.
Solution: network_mode: host in docker-compose — container shares host networking.
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.
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.
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.
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.
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.
Problem: Crash in DDRAW.dll when loading a map — DDrawCompat didn't work with llvmpipe.
Solution:
- Renamed
DDrawCompat.initoDDrawCompat.ini.disabled WINEDLLOVERRIDES="ddraw=b"— forced Wine's built-in ddraw
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:i386Then docker commit homm3 homm3:latest to preserve changes.
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 -noxdamagefor 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.
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.
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.
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-accessgroup
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.
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
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.
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.
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
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.
-
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.
-
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.
-
Cloudflare Tunnel audio — configured on separate subdomain (homm3-audio.pg7812.org). Works but requires two DNS entries per deployment.
-
Auto-save persistence edge case — if someone runs
docker compose down -v, the named volumehomm3-saveswill be deleted along with Wine prefix and saves. Safe options:docker compose down(without -v),docker restart,--force-recreate.
# 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:6080Beyond what was in the Dockerfile, the following were manually installed:
libgl1-mesa-glx:i386 libgl1-mesa-dri:i386 libegl1-mesa:i386— 32-bit OpenGLtigervnc-standalone-server— TigerVNC (replaced Xvfb + x11vnc)pulseaudio— audio in system modeffmpeg— (optional, unused in final audio version)mesa-utils— glxinfo for diagnosticsbinutils— strings for DLL analysisimagemagick— import for diagnostic screenshots