Skip to content

Add mouselook eye-height offset and vehicle-tilt decouple option - #356

Closed
Shadowolf7 wants to merge 24 commits into
AlchemyViewer:developfrom
Shadowolf7:worktree-mouselook-eye-offset-vehicle-decouple
Closed

Add mouselook eye-height offset and vehicle-tilt decouple option#356
Shadowolf7 wants to merge 24 commits into
AlchemyViewer:developfrom
Shadowolf7:worktree-mouselook-eye-offset-vehicle-decouple

Conversation

@Shadowolf7

Copy link
Copy Markdown

Summary

  • Roadmap item 15, sourced from real user feedback relayed by Roger (vehicle builder "Tiff Haystack" relaying customer complaints in-world).
  • Adds AlchemyMouselookEyeHeightOffset (F32, meters, default 0.0): a user-tunable vertical offset applied in LLAgentCamera::calcCameraPositionTargetGlobal(), since mouselook eye height is currently derived straight from the avatar skeleton's head-joint Z position, which commonly mismatches mesh body/head visual eye placement.
  • Adds AlchemyMouselookDecoupleVehicleTilt (bool, default off): checked alongside the existing FLAGS_CAMERA_DECOUPLED server-flag check in LLAgentCamera::calcFocusPositionTargetGlobal(), letting a passenger opt out of the mouselook view tilting with vehicle roll/pitch even when the vehicle's own script hasn't set that flag. Scoped to rotation only — camera position is untouched.
  • Also threads the eye-height offset through the separate AlchemyRealisticMouselook override path in updateCamera() (~line 1558), which otherwise bypasses calcCameraPositionTargetGlobal() entirely while standing and would have left the new slider inert for anyone with that setting on. Applied to both the camera origin and look-at target so it doesn't introduce pitch.
  • New Preferences > Move > Mouse Input controls: a checkbox for the vehicle-tilt decouple, and a slider+spinner for the eye-height offset (range ±0.3m).

Verification

  • No local build environment run for this change (build machinery not exercised end-to-end). Syntax-checked llagentcamera.cpp with -fsyntax-only against the project's real compile_commands.json flags (return code 0, no diagnostics).
  • XUI layout change verified with a script that walks the top/top_pad/top_delta chain in document order (per this repo's established practice for this kind of check) — new content bottoms out at 348px against 435px of panel height, no overflow. Confirmed panel_preferences_move_mlook.xml is live (embedded as the "Mouse Input" tab in panel_preferences_move.xml), not dead.
  • Not runtime-tested: no confirmation the offset visibly moves the camera in-game, that decoupling actually levels the view on a rolling vehicle, or that the new controls render as expected. Whoever tests this: skins/ is only copied into the build output at CMake configure time, not on every incremental build — a stale skins dir would make the new controls appear missing even though they're present.

Test plan

  • Configure + build, confirm the viewer compiles
  • Preferences > Move > Mouse Input: confirm the new checkbox and slider/spinner appear correctly, don't overlap neighboring controls
  • Enter mouselook standing, adjust the eye-height slider, confirm the view moves vertically
  • Sit on a physics vehicle with roll/pitch, enable "Keep mouselook level while seated on a vehicle", confirm the view stays level while driving
  • Repeat the eye-height check with "Realistic mouselook" also enabled while standing

🤖 Generated with Claude Code

Roger Luedecke and others added 24 commits July 30, 2026 23:31
Renames the user-facing/OS-facing product identity across build
config, macOS bundle IDs (viewer + CEF plugin), Windows resource
metadata, in-app strings, installer/Linux packaging scripts, and
docs. Adds placeholder icon/logo art under indra/newview/branding/
(replacing the external alchemy-branding vcpkg dependency for now)
and doc/vayu_logo.png.

Internal code identifiers (settings keys, shader dir names, vcpkg
triplets/ports, the "alchemy" skin theme) and .github/ CI workflows
are intentionally left untouched — see the plan notes for why.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- gGLManager.mHasBPTCTextureCompression detects GL 4.2 core / GL_ARB_texture_compression_bptc
- setManualImage's runtime compression path now prefers explicit BC7 over
  generic driver-chosen formats when available, sharing one lookup table
  (compressed_internal_format) so the swap and its detection can't drift apart
- Fixes an isCompressed() regression: widening it to also detect the
  generic-compression-on-upload case broke setImage()'s data_hasmips
  branch, which relies on isCompressed() to tell raw pixels from
  pre-compressed blocks. Split into isCompressed() (source-precompressed,
  e.g. DXT containers) and willCompressOnUpload() (will be compressed by
  setManualImage), each used only where they're actually the right question.
- setSubImage()/setSubImageFromFrameBuffer() now refuse partial updates on
  a compressed-or-will-compress texture instead of issuing a GL call
  (glTexSubImage2D/glCopyTexSubImage2D) that's illegal on compressed
  internal formats -- previously silent corruption, now a clear LL_ERRS
- dataFormatBits/dataFormatBytes/dataFormatVRAMBytes/dataFormatComponents
  didn't know the BC7 enums, so any BC7-compressed texture's memory
  accounting hit LL_ERRS("Unknown format") the moment it was allocated

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
FMOD Studio on Linux:
- FMODSTUDIO.cmake looked for libs under api/core/lib/x64, but the Linux
  SDK ships them under x86_64 -- find_library never matched
- lllistener_fmodstudio.cpp/llaudioengine_fmodstudio.cpp reinterpret-cast
  LLVector3's float array to FMOD_VECTOR* and dereferenced it, which is
  strict-aliasing UB and a hard error under -Werror=strict-aliasing on
  current GCC; build by value instead (to_fmod_vector helper)
- viewer_manifest.py never bundled libfmod.so* into the package (FMOD
  isn't a vcpkg dependency, so nothing copied it) -- binary linked against
  it but the .so was never shipped, so the packaged viewer couldn't start.
  New --fmod= manifest arg / FMOD_MANIFEST_LIB_DIR CMake var fix this.

Packaging: the "Viewer Manifest Copy" post-build step passed
--actions=copy alone. is_packaging_viewer() (gates whether shaders, fonts,
skins, app_settings XML etc. are even considered) requires "package" in
the actions list, but only copy_action() (needs "copy") actually writes
files -- package_action() alone is a no-op. So this step silently produced
a package missing nearly everything needed to run, without failing the
build. Now passes "copy package" together, matching the LOCAL_DIST_DIR
block's existing pattern. strip_binaries() (package_finish) is a no-op
for non-Release buildtypes, so normal dev builds keep their debug info.

Launcher: refresh_desktop_app_entry.sh only reinstalled the .desktop
entry when its content actually changed (previously unconditional on
every launch), added PrefersNonDefaultGPU=true, and exposes a "Launch
with Zink (OpenGL-over-Vulkan)" right-click action via a new vayu-zink
wrapper (gamemoderun + switcherooctl launch, degrading gracefully if
either tool is absent). wrapper.sh carries over AMD performance tuning
(AMD_DEBUG=lowprecision, mimalloc) from the Firestorm high-performance
launcher.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
Auto-detects ccache and sets it as the C/C++ compiler launcher before
project()/enable_language() run (where CMake starts honoring
CMAKE_<LANG>_COMPILER_LAUNCHER). Silently a no-op if ccache isn't
installed, so this is safe to leave on by default. Speeds up rebuilds
after a git pull/rebase that touch many files but leave most preprocessed
output unchanged -- Ninja alone recompiles anything with a newer mtime
even if the content is identical to something already compiled.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
LLViewerDynamicTexture (snapshot previews, floater thumbnails, etc.) and
the terrain paintmap baking texture both refresh via
setSubImageFromFrameBuffer(), which LLImageGL now refuses on a
compressed-or-will-compress texture (partial/copy updates are illegal on
compressed internal formats). Neither disabled compression on its
texture, so enabling RenderCompressTextures hit that guard immediately.
Fix mirrors the existing setAllowCompression(false) pattern already used
for media textures and UI images.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TyvmxbNsTvqm79FvJRUP3J
Completes the drop of the vendored, patched curl 7.54.1 (which existed
solely for an old HTTP/1.1 pipelining fix that no longer applies --
modern curl dropped real HTTP/1.1 pipelining years ago, making
CURLOPT_PIPELINING a no-op regardless) in favor of stock vcpkg curl.

That drop exposed a real, previously-masked bug: Linden Lab's asset
CDN sends "Content-Encoding: binary/octet-stream" on essentially every
asset response (a MIME type, not a transfer coding -- almost certainly
swapped with Content-Type in the S3 object metadata upstream). Old
curl silently tolerated an unrecognized Content-Encoding value; curl
8.21.0 treats it as a hard failure (CURLE_BAD_CONTENT_ENCODING /
"Easy_61"), which was surfacing as near-100% mesh header and texture
fetch failures -- explaining why meshes and sculpts stopped loading,
especially right after a teleport into a busy region.

Add a per-policy-class PO_CONTENT_DECODING option to llcorehttp and
disable it for the binary asset-fetch classes (texture, mesh1, mesh2,
large mesh) that talk to the CDN, while leaving it enabled for
capability calls (inventory, agent, etc.) that legitimately use gzip.
Both CURLOPT_ENCODING and CURLOPT_HTTP_CONTENT_DECODING are set
explicitly on every request rather than left unset when disabled,
since libcurl easy handles are recycled from an unreset pool
(HttpLibcurl::HandleCache) and would otherwise carry a prior request's
decoding state forward.

Also restore the QAModeHttpTrace debug setting (referenced in
_httpoprequest.cpp but missing from settings.xml, so libcurl verbose
tracing had no way to be enabled) -- it's what surfaced the bogus
response header in the first place.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J7R2Za4br1N947HEDgf8zr
LLMeshRepoThread::run()'s header-fetch loop silently dropped a request
on final retry exhaustion (just a debug log), unlike the LOD and skin
loops right next to it, which push into mUnavailableQ on the identical
exhaustion condition so waiting objects get unstuck. Any LLVOVolume
waiting on that mesh's header never received notifyMeshLoaded or a
forced LOD fallback, leaving it at its placeholder shape permanently
with no further retry.

fetchMeshHeader() returns false when the local HTTP layer refuses to
even enqueue the request (congestion), which is rare in ordinary play
but close to guaranteed right after a teleport into a busy region, when
a whole region's object list arrives at once and hits the fixed
concurrent-request cap -- matching the reported symptom of mesh
objects never rezzing after teleporting into crowded regions.

Mirrors LLMeshHeaderHandler::processFailure(): push all LOD levels for
the mesh onto mUnavailableQ so dependent objects fall back instead of
waiting forever.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J7R2Za4br1N947HEDgf8zr
…world alpha

Depth-splices rigged attachment alpha into the distance-sorted world
alpha walk instead of rendering rigged-then-world in two passes, so
transparent geometry composites correctly both in front of and behind
avatars (e.g. hair vs. background trees). Gated by RenderInterleavedAlpha
(default on); legacy two-pass order remains available by disabling it.

Ported from secondlife/viewer#5927
(Viscerous/viewer feature/interleaved-alpha), adapted for local
divergence:
- lldrawpoolalpha.cpp: dropped the GLTF-scene depth-buffer block from
  upstream's diff since LL::GLTFSceneManager was already removed here
  (9a6ace9, "Remove dead PMFP render pipeline and scene manager");
  kept this fork's simplified sRenderingHUDs-free debug-alpha condition
  from 47b8fb7/c62735adc8 and only applied the EAlphaStream rename.
- llspatialpartition.h: added <functional> alongside this fork's
  boost::unordered_map include rather than upstream's std <queue>/
  <unordered_map>, and hand-added mAvatarDepth to LLSpatialGroup to
  match this fork's member layout.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…d windlight quick-select

Four Firestorm-inspired features, adapted to this fork's existing systems
rather than ported verbatim where the underlying APIs differ:

- Pose stand: fsfloaterposestand.cpp/h ported from Phoenix Firestorm,
  reusing fspose.cpp/h (already present but never wired into
  CMakeLists.txt since the April 2026 Poser merge). Uses
  LLAgent::stopCurrentAnimations(force_keep_script_perms) -- ported
  Firestorm's FIRE-12148 fix alongside it, since dropping that flag
  would silently revoke script animation permissions (e.g. XPOSE) every
  time the pose stand opens. New "not_sitting" Agent.IsActionAllowed
  case gates the menu item and toolbar command.

- FPS limiter: new ALLimitFramerate/ALFrameRateLimit settings gate a
  sleep-based frame cap in LLAppViewer's main loop, ported from
  Firestorm's FSLimitFramerate. Vayu had no equivalent -- the existing
  sleeps in that loop are all background-yield/IO-throttle, not a
  foreground cap.

- VRAM draw-distance toggle: new ALDrawDistanceVRAMOptimization setting
  (default on, preserving existing behavior) gates the
  sDesiredDiscardBias-driven draw-distance reduction in
  llviewerdisplay.cpp, matching Firestorm's FIRE-35748 -- this behavior
  already ran unconditionally here, so the change is additive only.

- Windlight quick-select: extends the existing ALPanelQuickSettings
  panel (status-bar quick-settings button) with sky/water/day-cycle
  preset combos and a reset-to-region-default button, built against
  this fork's actual inventory-item-based LLEnvironment API rather than
  porting Firestorm's older flat-file preset scan. RLV-gated on
  RLV_BHVR_SETENV to respect @setenv=n restrictions.

Also fixes a pre-existing build bug found while testing this: the
POST_BUILD viewer_manifest.py invocation passed --actions as an
escaped-quote CMake string that didn't survive Ninja's re-escaping,
arriving as ['copy\', 'package'] instead of ['copy', 'package']. Since
copy_action() is gated on the exact "copy" action being present, every
non-packaging dev build was silently skipping is_packaging_viewer()'s
file staging (app_settings, skins, shaders, etc.) -- builds "succeeded"
but ran against arbitrarily stale staged assets.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nfig workaround

wrapper-zink.sh now just sets the Zink override and hands off to wrapper.sh
instead of duplicating the gamemoderun/switcherooctl launch logic, so
Zink-mode launches get the same GameMode/GPU-selection behavior as normal
launches without a second copy of the logic to keep in sync. Also skip both
under LL_WRAPPER so gdb/valgrind troubleshooting gets a plain process tree.

OPENSSL_CONF=/dev/null works around the statically-linked OpenSSL trying to
read the system's crypto-policy config on distros (openSUSE/Fedora) whose
config properties it doesn't recognize, which fails config parsing on the
first TLS handshake and gets misreported by curl as CURLE_OUT_OF_MEMORY.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Git worktrees do not inherit submodule checkouts from the main clone, so
`git worktree add` produces a tree with indra/dullahan and vcpkg both
empty. Neither resulting failure names the cause: dullahan surfaces as a
bare "does not contain a CMakeLists.txt" from add_subdirectory() roughly
1100 lines into the log, after a full vcpkg install has already run, and
vcpkg surfaces as a missing CMAKE_TOOLCHAIN_FILE.

Detect both up front -- ahead of the first project() call, so it runs
before the toolchain is loaded -- and run `git submodule update --init
--recursive` when either marker is absent. If git is missing, or the
markers are still absent afterwards, fail immediately with the exact
command to run rather than letting the confusing downstream error happen
anyway.

On a normal checkout this is two EXISTS tests and nothing else; the git
call only ever runs when a submodule is genuinely missing.

Verified by reproducing the snag in a throwaway worktree: both submodules
were absent, the guard detected them, cloned both, and configure
continued through vcpkg bootstrap.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ported from Cool VL Viewer's gUsePBRShaders/RenderUsePBR concept, adapted
to Vayu's architecture. Unlike CoolVL (which swaps its entire ee/ vs pbr/
shader tree and tears down render pools), Vayu already renders legacy
Blinn-Phong and GLTF PBR materials side by side in one unified deferred
pipeline, so this only needs to gate the few places that route a face to
the PBR draw pool and shading path:

- LLPipeline::getPoolTypeFromTE(): stop routing GLTF-materialed faces to
  POOL_GLTF_PBR when the setting is off, so they fall through to the same
  pool selection an ordinary legacy face would get.
- LLVolumeGeometryManager::registerFace(): stop nulling the face's legacy
  diffuse texture when the setting is off. That nulling assumed the PBR
  pool's own per-channel textures would be bound instead; skipping it lets
  the face's legacy fallback diffuse texture (the same texture non-PBR
  viewers already see, per SL's existing backward-compat convention for
  GLTF materials) get bound normally.
- LLFace::canRenderAsMask(): let a de-PBR'd face qualify for alpha-masking
  like any other legacy face instead of being unconditionally excluded.

Reflection probes, hero probes, SSR, and the GBuffer ORM slot are
intentionally left untouched -- they're already independently controlled
via their own settings and aren't part of what this toggle is for.

Known gap: toggling the setting doesn't force already-rendered geometry to
immediately re-evaluate its pool assignment, so visible objects only pick
up the change on their next natural rebuild (LOD change, region change,
relog) rather than instantly. Left alone rather than guessing at a
rebuild-all mechanism without a way to test it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Notecards had no way to load content from disk (only copy/paste or the
external-editor round trip); scripts already had one via the File menu's
"Load from file..." item. Add a "Load..." button to the notecard preview
floater using the same async LLFilePickerReplyThread pattern, feeding
into the existing loadNotecardText() path (plain setText, not
importBuffer -- that's reserved for the "Linden text version" container
format used for embedded-item round trips).

Both loadNotecardText() (notecard, also used by the external-editor sync
path) and LLScriptEdCore::loadScriptFromFile() (script) now strip a UTF-8
BOM and normalize CR/CRLF line endings, since files picked off disk may
not be ASCII/LF like our own tmp-file round trips. The script path
previously reconstructed the buffer with getline(), which left stray \r
characters on Windows-authored files; it now reads the file in binary
mode and normalizes explicitly.

Verified LLEmbeddedItems::removeUnusedChars() scans live text and
self-prunes stale embedded-item references at save time, so replacing a
notecard's text via the new load button cannot leave dangling embedded
items.

Known pre-existing gap, not touched here: loadScriptFromFile/
saveScriptToFile pass a raw LLScriptEdCore* across the async file-picker
callback rather than a safe LLHandle, so closing the floater while the
OS file dialog is open could dangle. Left as-is to keep this change
scoped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Some notecard authors include a line made of one repeated character
purely to indicate the character width the content was formatted for
(word-wrap/ASCII-art alignment assumes that width). Detect the longest
such line (minimum 20 chars, to avoid tripping on ordinary short
dividers like "-----") and, when found, switch the notecard editor to
a monospace font and resize the floater to fit -- the convention is
meaningless in a proportional font, since character widths vary and
the notecard editor currently uses SansSerif.

Applied everywhere notecard text gets (re)loaded: the initial asset
load and loadNotecardText() (shared by the external-editor sync path
and the "Load..." button added earlier).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch upload only recognized texture, sound, and animation files
(LLResourceUploadInfo::findAssetTypeAndCodecOfExtension); .txt/.lsl/
.luau files were silently skipped even though the bulk-upload file
picker already accepts any file (FFLOAD_ALL).

- findAssetTypeAndCodecOfExtension now maps .txt -> AT_NOTECARD and
  .lsl/.luau -> AT_LSL_TEXT.
- LLAgentBenefits::findUploadCost now recognizes both as free (cost 0)
  -- notecards and scripts have always been free to create, unlike
  the L$-per-item texture/sound/animation costs this function
  otherwise reports. Without this, do_bulk_upload's cost-lookup gate
  would silently drop these files even with the extension recognized.
- LLNewFileResourceUploadInfo::exportTempFile gains a plain-text
  passthrough branch: no transcoding needed (unlike image/sound/
  animation), just the same UTF-8 BOM strip + CRLF normalization
  already applied to the single-file notecard/script load paths,
  since this may be an arbitrary file off disk.

No changes needed in do_bulk_upload itself or the file picker filter
-- both the cost-estimate and real-upload code paths already route
non-texture types through findUploadCost/findAssetTypeAndCodecOfExtension
generically, same as sound/animation today.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The POST_BUILD and LOCAL_DIST_DIR custom commands passed
--actions="copy package" to viewer_manifest.py with the quotes baked
into the CMake string literal. Without VERBATIM, that survives into
the literal shell command ninja runs as --actions="copy\ package";
the shell's double-quote parsing doesn't treat backslash-space as an
escape, so Python receives the raw value copy\ package and
llmanifest.py's whitespace .split() corrupts it into ['copy\\',
'package'].

is_packaging_viewer() still matched, since the exact 'package' token
survived, so construct() kept registering every skins/app_settings/
character/fonts file. But process_file()'s per-file dispatch looks up
a method named "<action>_action" for each action: 'copy\\_action'
doesn't exist and is silently skipped via getattr(..., None), and
'package_action' is a documented no-op. Net effect: every local build
copied zero resource files into RelWithDebInfo, while the compiled
binary kept landing there fine via CMake's own link step, independent
of viewer_manifest.py. install.sh then faithfully cp -a'd an
already-stale resource tree next to a fresh binary.

Fix is to drop the redundant embedded quotes -- CMake's own escaping
already handles the space correctly (confirmed against the regenerated
ninja command line), same as the working --channel="Vayu Test" pattern
elsewhere in this file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Batch/bulk upload already supported notecards and scripts, but there
was no single-file equivalent to Image/Sound/Animation/Model/Material
-- only the "Bulk..." picker.

Neither asset type has an upload-preview floater the way image/sound/
anim do, so upload_single_file's floater-name switch isn't a fit for
them (its default case is a silent no-op, same failure shape as the
bulk upload used to have before .txt/.lsl/.luau were recognized).
Instead, upload_single_notecard_or_script() reuses the direct
upload_new_resource() path do_bulk_upload already takes for these two
types once the extension is recognized -- no preview step, since both
are free to create.

FFLOAD_SCRIPT already existed and needed no filter changes.
FFLOAD_NOTECARD is new, wired into all three llfilepicker.cpp platform
blocks (Windows OFN, GTK/portal, legacy macOS) the same way
FFLOAD_SCRIPT is, plus build_extensions_string so extension validation
isn't a no-op on Windows.

Menu items are unmetered labels (no [COST]/Upload.CalculateCosts),
matching Model/Material rather than Sound/Animation, since
LLAgentBenefits reports 0 cost for both and CalculateCosts doesn't
know about either asset type.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "Limit framerate" checkbox and FPS slider (added alongside the
Firestorm-ported frame limiter) landed at computed Y 317-333 in
Preferences > Graphics, directly under the Save/Load/Delete preset
buttons, which used a hardcoded top="310" left over from before those
controls existed. Full vertical and horizontal overlap -- confirmed by
simulating the XUI top-down layout flow rather than eyeballing pixel
math by hand.

Switched the preset-button row to flow relatively (top_pad="14") off
the new content instead of a stale absolute position, so it and
everything chained below it (Defaults/Advanced Settings buttons) stays
clear regardless of what's added above. Still fits within the panel's
declared height with margin to spare.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The status-bar quick-settings panel's sky/water/day-cycle preset
combos sat under one generic "Environment:" heading with nothing
distinguishing which dropdown controlled what.

Checked Firestorm's floater_quickprefs.xml for the reference pattern
-- it labels each combo individually ("Sky:", "Water:", "Day Cycle:").
Added the same per-dropdown labels here, using this panel's own
existing label idiom (label pulled up beside its control via negative
top_pad, already used for Draw Distance/LOD/etc. above it) rather than
importing Firestorm's separate same-row layout mechanics, so it stays
visually consistent with the rest of the panel. Kept the "Environment"
heading as a section divider from the quality/performance settings
above it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The "PBR Materials" checkbox (RenderPBRMaterials) in Advanced Graphics
Preferences overlapped the Reset/OK/Cancel button row -- computed Y
419-435 vs. the button row's Y 422-445, same horizontal range. That
row uses a hardcoded absolute top="415" rather than flowing, because
this floater has two side-by-side columns sharing one coordinate
stream and the standard top_pad trick only tracks the immediately
preceding document-order sibling, not whichever column is actually
tallest -- so a relative fix risked being correct today and wrong the
next time either column grows. Bumped the absolute position instead
(415 -> 440) to clear the checkbox's new bottom, and grew the
floater's declared height (452 -> 477) to keep it from clipping the
button row, preserving the original ~7px bottom margin.

Also corrected the tooltip. It previously implied unchecking the
setting reveals a texture SL guarantees via "existing backward-compat
convention" -- checked the actual SL Wiki/community documentation and
that's not accurate: a good legacy fallback texture is optional
creator best practice, not a system guarantee, and the untouched
default is just SL's blank/wood texture. Rewrote it to say that
plainly, and to spell out that pool routing and alpha-mask eligibility
fall back too (not just shading), and that reflection
probes/hero-probes/SSR/AO are unaffected since they're scene-wide
systems controlled separately.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Roadmap item 15, sourced from real user feedback (vehicle builder Tiff
Haystack relaying customer complaints): mouselook eye height has no
tunable offset and is derived straight from the avatar skeleton's head
joint, which commonly mismatches mesh body/head visual eye position;
and the mouselook view tilts with vehicle roll/pitch unless the
vehicle's own script opts out via FLAGS_CAMERA_DECOUPLED, which a
passenger can't control.

Adds AlchemyMouselookEyeHeightOffset (F32 meters) applied in
calcCameraPositionTargetGlobal(), and AlchemyMouselookDecoupleVehicleTilt
(bool) checked alongside the existing server-flag check in
calcFocusPositionTargetGlobal(). Also applies the eye-height offset to
both the origin and look-at target in the AlchemyRealisticMouselook
override path in updateCamera(), which otherwise bypasses
calcCameraPositionTargetGlobal() entirely while standing.

Decoupling is scoped to rotation only, per the roadmap note's open
question left unresolved by choice.
@coderabbitai

coderabbitai Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9633472c-b473-4145-8150-15f0441f67d7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@Shadowolf7

Copy link
Copy Markdown
Author

Opened in error against the wrong repository (should have targeted a fork, not upstream). Closing immediately — no review needed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant