Skip to content

Introduce skid_steer as a new way to load assets - #515

Open
patowen wants to merge 6 commits into
Ralith:masterfrom
patowen:introduce-skid-steer
Open

Introduce skid_steer as a new way to load assets#515
patowen wants to merge 6 commits into
Ralith:masterfrom
patowen:introduce-skid-steer

Conversation

@patowen

@patowen patowen commented Aug 17, 2026

Copy link
Copy Markdown
Collaborator

This is a relatively significant refactor of Hypermine, switching up asset loading to use a new library called "skid_steer".

It introduces the following changes:

  • A custom AssetLoader type is added to drive asset loading, acting as the glue between skid_steer and Hypermine. This completely replaces the old loader module.
  • All uses of the staging buffer and transfer module from the temporary lahar_deprecated module have been replaced with a vendored growable_ring implementation from @Ralith, and a ParallelQueue from the current version of lahar. This allows us to finally remove the lahar_deprecated module, a task that I had hoped to do years ago.
  • PNG loading and GLTF loading, the only two processes that depended on the old Loader, have been migrated to the new AssetLoader.
  • File-processing (reading PNGs and GLBs) and Vulkan-wrangling (making Vulkan calls) have been fully separated, with all data being read into memory in a convenient form before being passed into Vulkan data structures.
  • To support some of the above changes, some data has been moved around, including the addition of a new ShaderData struct to pass around global data used for rendering.

@patowen
patowen force-pushed the introduce-skid-steer branch 3 times, most recently from 614549c to 656444b Compare August 22, 2026 02:41
.image(handle)
.subresource_range(range)],
);
xf.device.cmd_clear_color_image(

@patowen patowen Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When separating out gtlf-reading logic with Vulkan wrangling, I had decided to remove the functionality that uses cmd_clear_color_image to fill the 1x1 image with a specified color, in favor of just specifying the color directly.

However, this requires a conversion from f32 color to SRGB, which you might have opinions on how to do. One option could be to import something like https://crates.io/crates/palette (although you might have a preferred crate for this). Given that we might want to do more in the future, I don't think we want to hardcode the gamma correction formula.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I haven't gotten to the context of this question yet, but the color crate is an alternative to consider for color space conversions; it's maintained by some extremely reputable 2D graphics folks. I don't think we need an external crate for gamma conversion but it's fine to use one.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I might consider the color crate. Although implementing it myself (or copying color's implementation would be pretty simple, it's complicated by the fact that the gamma conversion function is piecewise, starting linear and ending with a power function, and I also have to convert from a float to a u8, which has its own rounding standards for color (that I don't remember off the top of my head). My understanding is that when something is messy due to existing standards, it's a good sign that it would be best to reuse an existing implementation.

Comment thread client/src/graphics/gltf_mesh.rs Outdated
.material()
.pbr_metallic_roughness()
.base_color_factor()
.map(|c| 255) // TODO: Will likely want a crate for color conversion

@patowen patowen Aug 22, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://github.com/Ralith/hypermine/pull/515/changes/BASE..0908719a3be8e59af41e30e075cbe97f0001cf17#r3834892686.

I deliberately kept the lint failing due to this line so that I wouldn't forget to address this.

EDIT: Actually, now that there's an unresolved PR comment, this shouldn't be forgotten anyway. I'll fix the lint.

@patowen
patowen requested a review from Ralith August 22, 2026 03:06
@patowen
patowen force-pushed the introduce-skid-steer branch 2 times, most recently from 2ac99ab to 359c1aa Compare August 25, 2026 02:21
@patowen
patowen marked this pull request as ready for review August 26, 2026 02:39

@Ralith Ralith left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review is just getting started, but it may be a few days before I make more progress.

Comment thread client/src/graphics/asset_loader.rs
Comment thread client/src/graphics/asset_loader.rs
Comment thread client/src/graphics/asset_loader.rs Outdated
}
}

fn test_eventual_success(mut f: impl FnMut() -> Result<(), anyhow::Error>) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is complex and may still be fragile; there's no hard guarantee on a desktop OS that any particular thread will be scheduled in any particular time interval. Prefer to actually wait for the event we're interested in. For example, you could use a std::sync::Condvar to implement a helper that blocks until an event is delivered, and call that in a loop.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see how that would make it more robust. Either the condition variable is triggered, and the test passes, or the condition variable is not, and the test hangs. In the latter, I would prefer the test to fail with a timeout error. The three-second timeout I added should be orders of magnitude longer than it would actually take.

I feel like if the asset loader hangs for 3 seconds for no discernible reason, that would be a bug in the asset loader, not a bug in the test.

While the function feels complex, at it's core, it's quite simple: Keep trying until it succeeds, or until it's time to give up.

If we want to be more robust than that, we'd need to dig into the implementation details of skid_steer and detect when it's no longer going to make progress.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the latter, I would prefer the test to fail with a timeout

Condvar with wait_timeout, then?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That might work. I guess the idea is to specialize this function to wait for events to show up instead of waiting an arbitrary amount of time with exponential backoff.

@patowen patowen Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My next force-push will include an attempt to resolve this.

EDIT: Done

Comment thread client/src/graphics/asset_loader.rs Outdated
Comment thread client/src/graphics/asset_loader.rs Outdated

/// Timeline sempahores must be signaled in a strictly increasing sequence, so having multiple threads manage
/// the semaphore that unparks the timeline queue is error-prone. The purpose of this thread is to centralize
/// management of this semaphore.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we actually need to trigger this from multiple threads? If so, would it be simpler to use a Mutex<u64> that we hold across the signal operation?

@patowen patowen Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since sending work for submission does not on its own unpark the parallel queue, we need to trigger this every time we send work for submission. This probably would be simpler with a mutex. It can be tricky at times to weigh options between different synchronization primitives.

I do like this version because it only signals the semaphore even if everyone tries to signal it at the same time, although it's unclear whether that's worth running an extra thread.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Signaling should not be an expensive operation.

@patowen patowen Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My next force-push will include an attempt to resolve this.

EDIT: Done

Comment thread client/src/graphics/asset_loader.rs Outdated
Comment thread client/src/graphics/asset_loader.rs Outdated
.expect("runtime using this context should already be dropped");

// Fail-safe to ensure that the parallel queue is driven at least once after all work has been submitted before
// we drain the handle

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would that be needed? If we don't intend for it to be needed, then this will prevent us from detecting bugs with tests.

@patowen patowen Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, thinking about it, I'm not sure whether we need this fail-safe.

On one hand, we need it because if a skid_steer::Source submits work without waiting on it, trying to drain the handle without driving the queue would result in a deadlock. After all, in the standard flow, waiting on the queue is what generally triggers the queue to be unparked (due to the implementation of wait_for_completion).

On the other hand, no asset sources should do that, since it would be unsound in almost all situations. I generally prefer crashes or validation errors over deadlocks though, since they tend to be easier to pin down.

Overall, I'm still in favor of keeping it. I think that it's more likely to ease debugging than make it more difficult (due to passing tests).

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How does this behavior help trigger a crash or validation error?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be more useful to detect that class of bug by panicking if a load task finishes without waiting for work it submitted?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced it is useful enough to be worth the extra scaffolding. The solution currently in place seems simplest, as it's essentially a way to ensure that the call to Handle::drain (which comes right after it) is preceded by a guarantee that the queue will be driven.

To answer your question on how it would help trigger a crash or validation error, it would ensure that the work sent for submission is actually submitted. If said work depends on resources that were already destroyed, that should trigger the validation error.

An alternative possibility is to let the deadlock happen, but in that case, I'd likely want to wrap it in a timer that at least logs a helpful warning if it runs out.

@patowen patowen Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thinking about this further, I feel like the current implementation is the one I prefer, as it follows the principle of least surprise, as we shouldn't need to expect draining the handle to deadlock. I consider unparking the semaphore to be a hard prerequisite to calling Handle::drain. After all, if the gap mentioned in #515 (comment) were closed, this would have happened automatically, and we wouldn't even be having this discussion (even though the soundness risk would still be present).

I don't think there's a clean, reliable, non-over-engineered way to check whether the load task waited for the work it submitted, since load functions are not currently required to report the necessary information to trigger this panic.

Comment thread client/src/graphics/asset_loader.rs Outdated
pub fn queue_family(&self) -> u32 {
self.gfx.queue_family
pub async fn wait_for_completion(&self, semaphore_value: u64) {
// To actually get the work to start, we need to unpark the queue. We do it here to avoid getting stuck awaiting something we never kicked off.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed? Submitting work should unpark the queue automatically.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Submitting work does not unpark the queue automatically. The only thing calling Work::end does is send it to the mpsc. The receiver of this mpsc only activates when ParallelQueue::drive is called, and that is what triggers the queue_submit function.

I should probably double-check the comments to make sure I say "sent for submission" instead of "submitted" in the appropriate places, since they are subtly different.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whoops, right you are. We could close that gap, but I guess it's nice to be able to batch stuff up.

Comment thread client/src/graphics/asset_loader.rs Outdated
queue_unpark_semaphore: vk::Semaphore,
shutdown_token: CancellationToken,
queue_watch_sender: &tokio::sync::watch::Sender<u64>,
staging: &GrowableRing,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider aggregating this stuff into a struct with a fn run(self).

@patowen patowen Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My next force-push will include an attempt to resolve this.

EDIT: Done

@patowen
patowen force-pushed the introduce-skid-steer branch from 359c1aa to cd2bff8 Compare August 28, 2026 04:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants