← Paracosm·Docs/Handbook
Contents · Your own viewer

Your own viewer: a patched Firestorm with a helper bridge

An assistant that builds a world needs to SEE it the way a person does, repeatably: put the camera exactly where it wants, take a clean picture, walk an avatar, read what is near it. Driving a person's desktop viewer through screenshots and mouse clicks works, but it is slow, fragile, and uses someone else's client. This chapter builds a separate viewer for the assistant, with its own settings and its own avatar, and a small HTTP bridge into the viewer's built-in helper-process API (LEAP).

Everything here is in the repo: viewer-patches/ (the source patches and the pinned base), viewer-build/ (the build and packaging scripts), tools/leap_bridge.py (the bridge), tools/leap_idle.py, tools/run_viewer.py (the launcher). The finished build is called Firestorm-Paracosm: a downstream modified Firestorm, not an official release.

1. Build Firestorm from source (Windows)

Firestorm is open source (LGPL), built on Linden Lab's viewer code. The recipe is in the repo: viewer-build/configure.bat then viewer-build/build.bat, then viewer-build/package.py for the portable zip. packaging.md has the full sequence.

2. The patches (viewer-patches/)

Ten patches in git format-patch form, one commit per change on top of the base 3125052b (each change's own follow-up fixes folded into it before the first release). Apply them in file order with git am viewer-patches/00*.patch: several change app_settings/settings.xml, llstartup.cpp or notifications.xml, each made against the one before it. The "Paracosm NNNN" numbers inside are the order the changes were planned in, and are what the source's <Paracosm> comments cite; the file numbers are the order they apply in, so file 0008 carries Paracosm 0004 (the reconnect mode: planned early, built last), and there is no Paracosm 0008.

file Paracosm no. what it does
0001-Paracosm-0001-its-own-settings-cache-and-saved-login.patch 0001 settings and saved logins in %APPDATA%/ParacosmViewer_x64, the cache in %LOCALAPPDATA%/ParacosmViewer_x64, so this viewer and an installed Firestorm never share state (llappviewer.cpp, lldir.cpp) The default cache really follows the app name (lldir_win32.cpp, lldir_linux.cpp built it before the name existed).
0002-Paracosm-0002-LEAP-camera-placement-setCameraPosAndF.patch 0002 two LEAP operations on LLAgent: setCameraPosAndFocus {camera_pos, focus_pos, obj_uuid?} places the camera exactly, detached from the avatar the way alt-clicking an object detaches it (the programmatic form of Object View), and resetCamera returns it to the avatar
0003-Paracosm-0003-LEAP-snapshot-format-png-or-jpeg-bmp-a.patch 0003 an optional format on the LEAP LLViewerWindow.saveSnapshot op: png (the default), jpeg or bmp
0004-Paracosm-0005-no-phone-home-by-default.patch 0005 no requests to third parties by default: a new FSDataDownload setting (off) stops the phoenixviewer.com data files, GridListDownload defaults off, and the OpenSim search and Marketplace URLs default empty
0005-Paracosm-0006-an-official-Linden-Lab-grid-takes-a-st.patch 0006 logging in to an official Linden Lab grid takes two stern confirmations (once per session), and every LEAP helper is terminated and refused for that session; with no grid chosen the viewer defaults to the first non-Linden grid, never Second Life A Linden grid is recognised at the moment of the login click (by name and login URI), and an unconfirmed Linden login is refused before anything is sent, by any route.
0006-Paracosm-0007-voice-off-by-default-and-Vivox-never-p.patch 0007 voice off by default, and Vivox never posts crash dumps (a private OpenSim world has no Vivox)
0007-Paracosm-0009-a-local-login-page-Firestorm-s-own-sit.patch 0009 the login screen's page: the Linden grids' page from Firestorm's own site and the legacy http://<grid>/app/login/ guess become skins/default/html/common/paracosm/login.html, shipped in the viewer and fetching nothing; a grid that names its own welcome page keeps it (fsgridhandler.cpp) The page is loaded directly (the login panel's LLURI rebuild lost a file: URL's path and showed black).
0008-Paracosm-0004-reconnect-mode-after-a-disconnect-a-re.patch 0004 reconnect mode: after a disconnect the viewer restarts itself, waits for the grid to answer and logs back in with the same account to the same place (section 6; paracosmreconnect.cpp, llappviewer.cpp, llstartup.cpp, llprogressview.cpp)
0009-Paracosm-0010-LEAP-relog-and-disconnect_state-on-the.patch 0010 LEAP relog {force?} and disconnect_state on the LLAppViewer event API (section 6; llappviewerlistener.cpp)
0010-Paracosm-0011-local-crash-minidumps-a-crash-writes-P.patch 0011 a crash writes a local minidump and a note into the viewer's logs folder, nothing sent (section 7; llappviewerwin32.cpp)

3. The bridge (tools/leap_bridge.py)

The viewer launches it as a helper (--leap) and talks to it over stdin/stdout. It serves HTTP on 127.0.0.1:9004 (bearer token): /snapshot, /camera, /orbit, /camera_reset, /position, /walk, /stop, /say, /nearby, /relog and /disconnect_state (section 6), and /call for any LEAP operation by name (for example {"pump": "LLFloaterReg", "op": "hideInstance", "name": "fs_nearby_chat"} closes a floater).

4. Launching it (tools/run_viewer.py)

5. What it is for

6. Reconnect mode (patch 0004) and the LEAP relog (patch 0010)

A region restart drops a logged-in viewer to "You have been logged out". Before 0004 the only way back was run_viewer.py --quit and a relaunch. Now the viewer does that itself, with the waiting built in.

Why a restart and not an in-process relog: once logged in, disconnectViewer() tears down the world, agent, inventory and region singletons, and the viewer has no "log out to the login screen" path that sets them up again in one process (Firestorm has none either; its grid switching happens before login). A fresh process is the clean state a login needs. The code is indra/newview/paracosmreconnect.cpp.

LEAP, on the LLAppViewer event API (the one requestQuit is on):

op request reply
disconnect_state {} connected, disconnected, startup_state, grid, linden, relogged (this viewer was started by a reconnect), session_started and now (epoch seconds), reconnect: {enabled, state, detail, reason, region, position, relaunch_in, probes, logins, next_in, waited_for_previous_ms, reconnects_last_hour}
relog {force?: bool} {ok, state, error?, relaunch_in?}. Disconnected: restart in a second and log back in (an explicit request overrides the automatic refusals and the hourly cap, not the hard ones: Linden grid, no credential, quitting). A viewer a reconnect started, waiting for the grid or between login attempts: try now. Logged in: refused unless force is true, then it logs out, restarts and logs back in.

state is one of idle, countdown, relaunching, refused, cancelled, failed (the disconnected viewer), waiting_for_grid, logging_in, retry_wait, done, gave_up, stopped (the viewer a reconnect started).

The bridge serves them as POST /relog {force?} and GET|POST /disconnect_state. It goes away with the old viewer and the new viewer starts a new one (it retries its port for a minute while the old one lets go); poll /state until it answers again. tools/region_restart.py uses them: once the region is up it asks the viewer (if a bridge answers) to relog now when its reconnect is waiting (countdown, refused, failed, waiting_for_grid, retry_wait), relogs it with force when it is still on a session older than the restart (it never noticed), leaves a cancelled one alone (a person chose "Stay disconnected"), and returns once a session newer than the restart is up. --no-viewer-relog skips it.

7. Crash minidumps (patch 0011)

The build has no BugSplat (--bugsplat= empty, so no LL_BUGSPLAT and no LL_SEND_CRASH_REPORTS), and the viewer excludes itself from Windows Error Reporting (WerAddExcludedApplication). A crash used to leave nothing but the log's last lines and a marker. Now an unhandled exception on any thread, a CRT invalid-parameter or pure-virtual-call error, and abort() write, into the viewer's own logs folder:

%APPDATA%\ParacosmViewer_x64\logs\Paracosm-crash-<YYYYMMDD-HHMMSS>-<pid>.dmp
%APPDATA%\ParacosmViewer_x64\logs\Paracosm-crash-<YYYYMMDD-HHMMSS>-<pid>.txt

The .txt names the exception, the faulting address as module + offset, the thread, and whether the dump was written. The dump is a minidump (threads, stacks, referenced memory, module list; a few MB), written by a helper thread with MiniDumpWriteDump from System32's dbghelp.dll; open it in Visual Studio or WinDbg with the build's firestorm-bin.pdb (build-vc170-64/newview/Release). The newest five are kept. Nothing is sent anywhere. The filter is installed first thing in LLAppViewerWin32::init, again after the window, GL driver and audio DLLs are loaded, and every 30 s after (a DLL that installs its own filter is logged once and overruled). Not caught: __fastfail (a /GS stack-cookie failure, heap corruption the OS detects) and a kill from outside, which by nature leaves nothing. To see one: Developer menu > Force an Error > Force Bad Memory Access.

8. The unclean exits during development (2026-09-25 to 27)

Every viewer session makes a dump-<uuid> folder in %APPDATA%/ParacosmViewer_x64/logs and removes it at the very end of a clean exit (removeDumpDir), and the exec marker Firestorm.exec_marker likewise. Eleven such folders were left, so eleven sessions did not exit cleanly (times local, UTC-8):

started evidence likely cause
09-25 17:42 and 09-26 17:17 the folder's static_debug_info.log: FatalMessage "llappviewer.cpp(3056) initConfiguration : Default configuration file settings_files.xml not found"; for 09-26 the Windows Application log has event 1000, firestorm-bin.exe, exception 0xc0000005 at offset 0x647f20 started from the wrong working folder (a development build reads app_settings/ from its working directory; section 1). LL_ERRS ends the viewer with a deliberate access violation: a crash by design
09-25 17:51 an empty folder: it died before writing its debug info very early; the same class or killed at once (no event)
09-25 17:55 the old bridge log (tools/leap_bridge.log): 17:55:32 "unparsable frame ... <? LLSD/Binary ?>", then 17:59:12 "viewer closed stdin; exiting" the bridge could not parse the viewer's binary LLSD (the llbase header mismatch, section 3), so --quit over LEAP could not work; the viewer was ended from outside
09-25 17:59, 18:28 bridge handshakes at 17:59:18 and 18:28:43, the next launches 36 s and 6 min later development iterations: a viewer ended from outside and relaunched
09-26 18:28, 18:46 no bridge in the bridge log; 18:28 logged in (STATE_STARTED, a login name, a region); 18:46 started while another viewer ran (FoundOtherInstanceAtStartup 1) viewers without the bridge, the operator's own login (run_viewer.py --as bob), closed from outside
09-27 08:43 Firestorm.old: the Linden-grid test of patch 0006, "Login failed" at 16:46:21Z, then the error dialog; the log stops at 16:48:43Z with no quit line killed at the error dialog to rebuild: the 0006 fix was committed at 08:53
09-27 09:29 Firestorm.log (the portable package): the last lines are "userQuit : User requested quit" and "Alert: Are you sure you want to quit?" at 17:32:09Z the process was ended while the quit confirmation was open

The Windows Application log holds no other Application Error or Application Hang event for the viewer since 09-25, and the System log no reboot: apart from the two wrong-folder starts, none of these was a crash the system saw. They were processes ended from outside (Task Manager, taskkill), which the viewer's next start reports as "Exec marker ... found; last exec crashed or froze". run_viewer.py --quit is not among them: it asks through LEAP (requestQuit) and that exit is clean.

What changes: after a region restart the viewer now restarts itself through a normal quit (no marker, no leftover folder) instead of being killed and relaunched, and a real crash now leaves Paracosm-crash-*.dmp/.txt beside the log, with the faulting module and offset. A kill from outside still leaves only the markers. Quit through LEAP or the window, and start the development build from indra/newview (run_viewer.py does both).