Skip to content

Runtime internals

Use this page when changing how dcmview selects an execution path, assembles a local viewer, performs blocking work, retains prepared frames, or shuts down. The source repository’s docs/architecture.md is the normative current-state model.

CLI / Python wrapper / VS Code
→ src/main.rs
→ application.rs
→ hidden --vscode-bridge-client form
→ VS Code bridge routing
→ local startup
→ bind listener
→ discovery coordinator
→ loader/
→ server/catalog.rs FileRegistry
→ annotations.rs AnnotationStore
→ server runtime
→ server/api routes and state
→ pixels display/raw services and caches
→ value mappings, overlays, and presentation layers
→ tags, semantic context, WSI context, and web assets
Embedded Svelte frontend
→ App.svelte
→ navigator, tabs, toolbar, viewport, tags, and status
→ frontend/src/api.ts
→ generated API types

main.rs owns command parsing and process exit. application.rs owns dispatch and logging setup: RUST_LOG overrides the default filter, and logs go to standard error because standard output carries the --startup-json lines. Bridge and local startup are mutually exclusive for a captured launch.

The routing rule lives in the binary. The Python wrapper and the terminal shims both launch through the hidden --vscode-bridge-client form, so every entry point follows the same rule: a process with the bridge environment (a VS Code terminal) may use any live bridge, and any other process routes only when its working directory is inside a registered workspace folder. Otherwise the local viewer starts in the same process.

Before a launch, each registry endpoint is probed without credentials; a live bridge answers 401. Any other answer, a closed connection, or a refused one marks the entry stale and removes it, while no answer in time falls through without removing it. A launch that reached a bridge without confirmation exits instead of starting a second, local viewer.

src/startup/ validates local options and the annotation CSV header, constructs registry and annotation state, binds the listener before discovery begins, starts the cancellable scan, registers stop-signal listeners, prints the URL, serves the viewer, and joins discovery before returning. server/runtime.rs owns the listener, browser, idle activity, and graceful shutdown resources. The listener is TCP or, with --unix-socket, a Unix socket owned by server/unix_socket.rs: it binds only in a directory the effective user owns and no one else can write, restricts the socket to mode 0600, accepts only peers with the server’s user ID, and removes the socket it created on shutdown. Ownership of the path is an exclusive lock on <socket>.lock held for the life of the listener; an existing socket is replaced only by a process that holds that lock. A socket launch always runs the local viewer and never opens a browser.

Startup also resolves the access token: the value of DCMVIEW_TOKEN, a generated one, or none under --no-token. server/api/auth.rs owns the token type and the check, which wraps the whole API router including its fallbacks, so a request without the token gets the same 401 unauthorized envelope whether or not the route exists. The --startup-json line is StartupEvent, owned by the crates/dcmview-protocol workspace member and re-exported from src/api/contracts.rs.

Stop signals (Ctrl+C and SIGTERM on Unix, Ctrl+C and Ctrl+Break on Windows) are registered before the URL is printed, because a wrapper may stop the process as soon as it reads the URL. With --startup-json, a completed scan that found files also prints {"type":"scan_complete","file_count":N}, which the Python wrapper waits for before returning a non-blocking handle.

The loader walks every input directory before inspecting candidates, parses each file’s header once, bounds each file’s frame_count by the frames it can hold, and counts skips by reason for the startup summary.

DICOM discovery runs through spawn_blocking and Rayon. Pixel decode and encode, tag-tree construction, semantic and value-mapping reads, overlay resampling, and other filesystem or CPU-heavy operations also cross a blocking boundary before doing that work.

Do not hold cache or registry locks while opening files, decoding codecs, windowing pixels, encoding PNGs, serializing tags, or awaiting another task. These invariants keep unrelated health and viewer requests responsive during long scans or expensive frames.

A cache miss registers an in-flight decode that later requests for the same key await. The decode runs as its own task and caches its result even if its client disconnected, and a semaphore bounds concurrent decodes to the core count.

Tests isolate this ownership through separate BoundServer::bind and serve stages and an in-process Axum router. Lifecycle integration tests cover auto-assigned ports, bind failures, external shutdown, idle timeout, activity resets, and draining an in-flight request.

src/api/contracts.rs is the source of truth for the HTTP contract: its endpoints table (method, path, response media type, response headers, success status), header names, query types, and every wire struct. Wire types derive ts_rs::TS, and cargo run --example generate_api_types writes the checked-in frontend/src/generated/api-types.ts; Svelte code reaches routes through frontend/src/api.ts.

When changing a route:

  1. Change the Rust contract.
  2. Regenerate the TypeScript contract with npm --prefix frontend run generate:types.
  3. Register or update the handler.
  4. Add a wrapper in the shared frontend fetch boundary.
  5. Extend contract and runtime tests.

See Internal HTTP protocol for the release’s route-level debugging reference.

The server’s display and raw-frame LRU caches are bounded by bytes only: by default 256 MiB for display PNGs and 384 MiB for decoded raw frames. Encoded SEG, dose, and Parametric Map overlays share a separate 64 MiB budget. --cache-budget sets one total that is split among the three in those proportions, each share rounded down; a frame larger than its cache’s share is served without being retained. Display keys include file, source frame, normalized window parameters, window mode, the unit of a real-world window, and the revision of the file’s redaction boxes. Raw keys include only file and source frame because window parameters do not change decoded sample bytes.

The raw cache is also the display path’s decoded tier for grayscale integer frames: a display miss on an 8-bit, 16-bit, or one-bit grayscale frame windows samples from the raw cache, decoding them there if needed, so one frame shown with several windows is decoded once. Frames larger than the display cache are shown uncached rather than failing. Tag trees, semantic contexts, and value mappings are cached per file in bounded LRUs.

The frontend retains resources for open tabs in independent byte-budgeted tiers, which --cache-budget does not change:

  • a 320 MiB compressed PNG payload cache;
  • a 128 MiB decoded RGBA bitmap working set; and
  • a 256 MiB raw-frame buffer cache.

The tiers are keyed by source frame and survive tab switches, so returning to a tab is served from cache. A new tab is prefetched within 48 frames of the current one, and the whole stack once the viewer has stayed on it for 1.5 seconds or cine plays. Held arrow keys step at most every 60 ms for frames or 150 ms for files. Foreground and prefetch raw consumers share in-flight work for the same source frame, and logical-tab teardown aborts that request registry.

These sizes and policies are implementation details, not public resource guarantees. Update the source architecture document and tests if they change.

App.svelte owns the shared root state through controllers in frontend/src/lib/app/: the catalog, tab and stack navigation, window settings (including the real-world unit), value overlays, and sidebar layout. The per-tab ViewStates store owns zoom, pan, and orientation; closing a tab forgets them. Active tool, cine settings, and semantic mode are plain App.svelte state. Individual components should not create competing roots of session state.

The viewport retains the last complete presentation until its replacement is ready, and a completed draw commits its layers, window, and frame label together. Cine advances only after the complete presentation is rendered. Every API response carries X-Server-Instance; api.ts rejects a response from a different server before parsing it, and the app reloads. api.ts also sends the access token with every request; a 401 replaces the viewer with the access-link page, or with the viewer-restarted page when the tab held an earlier session’s token.

The browser windows every single-channel frame up to 20 megapixels from raw samples, using the frame’s value mapping for Modality and VOI LUTs, and draws the frame’s presentation-layer above the image. Color frames and larger frames use display PNGs. A drag over a larger frame sends uncached preview=true requests, one in flight with only the newest window queued. Play with the W/L tool selected uses display frames with the current window.

Logical stacks retain (file index, source frame) identity across classic slices, multiframe files, and concatenations. Cache, metadata, reference, and annotation operations remain keyed to that source identity even when the UI shows one virtual sequence.

Semantic context remains separate from stored-pixel preview. A SEG composition is offered only after one compatible source mapping is validated, and it replaces the displayed image. While composed, ROI editing, cine, and interactive window/level stay disabled because their existing state belongs to the SEG object or requires another rendering contract. RT Dose and Parametric Map colorwashes are drawn on a separate canvas above the displayed image, so window/level, cine, ROIs, and the view transform keep working.

Every supported exit path must cancel and join discovery, drain in-flight requests during graceful shutdown, and release session state. The idle timeout does not start until the scan has finished and is then driven by request activity; browser asset requests reset it as well as API requests.

The server requires its access token on every API request and is loopback-bound by default. Preserve the non-loopback warning and the --no-token warning, never accept the token on the command line, in a query parameter, or in a cookie, and do not treat timeout, process ownership, or ephemeral storage as access control.