VkSplat Training Viewport Stability#1361
Conversation
There was a problem hiding this comment.
Pull request overview
This PR improves stability when starting training while the Vulkan/VkSplat viewport is active by preventing unsafe lifecycle churn of the VkSplat renderer/shared-scratch and by avoiding temporary camera-pause lease stacking.
Changes:
- Preserve
VksplatViewportRendereracross the training-start model pointer swap (while still resetting on non-training scene/model changes). - Prime VkSplat training shared scratch before the training thread starts, and make training-time viewport renders fail fast (retry/cached image) when the shared arena is unavailable.
- Prevent repeated camera movement from stacking temporary training pause leases by skipping the pause request when the underlying trainer is already paused.
Reviewed changes
Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/visualizer/rendering/rendering_manager.hpp | Adds API to pre-prime VkSplat training shared scratch from outside the render loop. |
| src/visualizer/rendering/rendering_manager_vulkan.cpp | Preserves VkSplat renderer across training model swaps; implements shared-scratch priming helper. |
| src/visualizer/rendering/vksplat_viewport_renderer.hpp | Declares ensureTrainingSharedScratchReady for pre-start setup. |
| src/visualizer/rendering/vksplat_viewport_renderer.cpp | Implements shared-scratch priming; tightens training-time arena-guard acquisition and “retryable” failure behavior. |
| src/visualizer/training/training_manager.hpp | Exposes isTrainerPaused() to distinguish UI state vs. underlying trainer pause. |
| src/visualizer/training/training_manager.cpp | Primes VkSplat training shared scratch between trainer init and training thread start. |
| src/visualizer/input/input_controller.cpp | Avoids stacking temporary pause leases during camera movement by checking underlying trainer pause state. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const std::size_t width = static_cast<std::size_t>(viewport_size.x); | ||
| const std::size_t height = static_cast<std::size_t>(viewport_size.y); | ||
| const std::size_t num_pixels = width * height; | ||
| const std::size_t num_tiles = | ||
| ((width + TILE_WIDTH - 1) / TILE_WIDTH) * | ||
| ((height + TILE_HEIGHT - 1) / TILE_HEIGHT); |
| if (!shared_scratch_.installed_in_training_arena || !shared_scratch_.block) { | ||
| return std::unexpected( | ||
| "VkSplat shared scratch training rasterizer arena is busy; reason=shared scratch is not installed before training render"); | ||
| } |
| if (auto* trainer = services().trainerOrNull(); | ||
| trainer && trainer->isRunning() && !trainer->isTrainerPaused()) { | ||
| trainer->pauseTrainingTemporary(); | ||
| training_was_paused_by_camera_ = true; | ||
| } |
| @@ -7035,7 +7087,9 @@ namespace lfs::vis { | |||
| shared_scratch_attempt_id = ++shared_scratch_attempt_serial_; | |||
| if (auto ok = ensureSharedScratchArena(context, required_shared_scratch); ok) { | |||
There was a problem hiding this comment.
this will fail because line 6609 creates the arena guard which prevents the arena from growing.
There was a problem hiding this comment.
Currently this will only break on massive resize of the viewport or in case we had just a huge densification.
I think this order must be ensured in the code:
ensureSharedScratchArena(...); // may grow, requires no active arena frame
shared_arena_guard.emplace()
| [[nodiscard]] bool isPaused() const { return state_machine_.isInState(TrainingState::Paused); } | ||
| [[nodiscard]] bool isFinished() const { return state_machine_.isInState(TrainingState::Finished); } | ||
| [[nodiscard]] bool isTrainingActive() const { return state_machine_.isActive(); } | ||
| [[nodiscard]] bool isTrainerPaused() const; |
There was a problem hiding this comment.
I think this is confusing here and does not fit properly into the list of methods:
How about something like:
bool TrainerManager::pauseTrainingTemporaryIfActive() {
if (!isRunning() || !trainer_ || trainer_->is_paused()) {
return false;
}
pauseTrainingTemporary();
return true;
}
|
thanks! Looks good. |
Summary
This change fixes an intermittent CUDA illegal-address failure seen on a 2080 Ti when training starts while the VkSplat viewport is active, especially when dragging or moving the viewport around training startup.
The fix keeps hardware decoding and the Vulkan viewport enabled. It does not exclude specific GPUs, disable nvImageCodec, or increase the VkSplat shared-scratch allocation estimate.
Root Cause
The failure was initially reported as nvImageCodec decode failures, but the detailed logs showed those errors were often downstream of an already-poisoned CUDA context. The first CUDA illegal address usually came from FastGS rasterization or related training work, while nvImageCodec later observed the same CUDA error during decode or sync.
The useful signal came from the Vulkan/VkSplat path:
VksplatViewportRenderer.cudaErrorIllegalAddressin FastGS, tensor ops, nvImageCodec, or Vulkan interop.So the core issue was not "hardware decode is bad" and not "the 2080 Ti must be disabled". The crash came from unsafe lifecycle/ownership around the VkSplat shared scratch during training startup and interactive viewport work.
What Changed
Preserve VkSplat Renderer During Training Model Swap
When
renderVulkanFrame()detects a model pointer change while training with the VkSplat backend, it now preserves the existingVksplatViewportRendererinstead of resetting it.This keeps the already-created handshake and shared-scratch state alive across the normal training-start model transition.
For non-training model changes, the previous reset behavior is kept.
Prime VkSplat Shared Scratch Before Training Starts
Training startup now asks the rendering manager to prepare VkSplat training shared scratch before the training thread begins.
This happens after trainer initialization and memory-pool trimming, but before
training_complete_is cleared and before the training thread starts. The goal is to have the renderer's shared scratch installed before FastGS starts using the CUDA arena.Fail Viewport Contention Safely
During training renders, VkSplat now checks that shared scratch is already installed and tries to acquire the rasterizer arena guard before doing input upload or other CUDA/Vulkan render preparation.
If the arena is busy, the viewport returns the existing retryable "shared scratch unavailable" path. The rendering manager can keep or return the cached viewport image and retry later, instead of performing late scratch setup while training owns the arena.
Prevent Camera Pause Lease Stacking
A second issue appeared after the crash was fixed: training sometimes did not resume after viewport/camera movement.
Logs showed repeated temporary camera pause requests at the same iteration while the underlying trainer was already paused. Those requests stacked temporary pause leases, so a later camera timeout could log "resuming" without actually reaching a zero pause depth.
Camera movement now only requests a temporary training pause when the trainer manager is running and the underlying trainer is not already paused.
Files Changed
src/visualizer/rendering/rendering_manager.hppsrc/visualizer/rendering/rendering_manager_vulkan.cppsrc/visualizer/rendering/vksplat_viewport_renderer.hppsrc/visualizer/rendering/vksplat_viewport_renderer.cppsrc/visualizer/training/training_manager.hppsrc/visualizer/training/training_manager.cppsrc/visualizer/input/input_controller.cppValidation
Tested manually on the affected Windows/2080 Ti setup with:
Observed results after the patch:
Build validation:
cmake --build build -j 8The build completed successfully.
Notes For Reviewers
The key behavioral change is that training-start model pointer changes are not treated the same as ordinary scene switches for VkSplat renderer lifetime. In training, preserving the renderer is intentional because the renderer owns CUDA/Vulkan interop state that must remain stable across the handoff into live training.
The retry path for busy shared scratch is also intentional. A missed viewport frame is acceptable; racing the trainer for shared rasterizer arena ownership is not.