Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Instance & Context

Earlier, I mentioned that it is possible to share lots of Vulkan context-building code between your applications, provided that you are ready to add extra configuration points to the context-building code whenever necessary in order to accomodate new needs.

In this chapter, we will provide a first example by adding a new configuration point to our instance-building code. This is necessary because in the main Gray-Scott simulation binary, we are using an indicatif-based textual progress bar. Which is good for user experience, but bad for developer experience, as it breaks one of the most powerful of all debugging tools: println!().1

Mixing indicatif with console output

Thankfully, the authors of indicatif are aware of the great sacrifice that fellow developers have to make when they use this library, so they tried to ease the pain by providing a println() method on the ProgressBar object that eases the migration of code that previously used println!().

However this method is not quite enough for our purposes, as we would like to follow Unix convention by sending our log messages to stderr, not stdout. So we will instead go for its more powerful cousin, the suspend() method. Its callback-based design lets us execute arbitrarily complex text output code and more generally use stdout and stderr in any way we like, without suffering the progress bar visual corruption that could otherwise ensue:

progress_bar.suspend(|| {
    println!("Can use stdout here...");
    eprintln!("...and stderr too...");
    println!(
        "...all that without losing access to {:?}, or needing to allocate strings",
        "the println mini-language"
    );
});

We can then leverage the fact that ProgressBar uses an Arc-like cloning model, which means that we can make as many clones of the initial ProgressBar object as we need, send them anywhere needed, and all resulting ProgressBar objects will operate on the same progress bar.

And by combining these two aspects of indicatif’s API, we can devise a strategy that will give us back a correct terminal display of Vulkan logs with minimal effort:

  • Send a copy of the ProgressBar to any code that needs to do some text output. If we are feeling extremely lazy, we could even make it a global variable, as we’re unlikely to ever need multiple progress bars or progress bar-related tests.
  • In the code that does the text output, wrap all existing text output into suspend().
  • Repeat the process every time new text output needs to be added.

Exercise

This is actually the only part of our Vulkan instance- and context- building code that needs to change in order to accomodate the needs of our Gray-Scott reaction simulation.

From the above information, we can infer a reasonably small code refactor that eliminates all risks of inelegant progress bar visual corruption:

  1. Add a new Option<ProgressBar> parameter to the logger_info() function in exercises/src/instance.rs, with the following semantics.
    • If this parameter is None, then there is no progress bar and we can just send output to the console directly the way we did before.
    • If it is Some(progress_bar), then wrap our Vulkan logging into a progress_bar.suspend(move || { /* ... logging goes here ... */ }) callback.
  2. Modify callers of logger_info()2 in order to give this parameter an appropriate value.
    • In the beginning, you will just want to add a similar extra parameter to the caller function, so that it also takes such a parameter and simply passes it down, repeating the process as many times as necessary until…
    • …at some point you will reach a top-level binary or benchmark that does not need to use indicatif. You will then be able to stop “bubbling up” optional parameters as described above, and instead simply pass None as an argument.
    • You will notice that examples/benches/simulate.rs does not need adjustments here (and does not compile yet). This is expected, that benchmark is pre-written in such a way that it will be valid by the time you reach the end of this section of the course.
  3. Finally, modify examples/bin/simulate.rs so that it sets up a LoggingInstance in an appropriate manner[^3]. For now, do not try to wire this object down through the rest of the Gray-Scott simulation. Just leave it unused and ignore the resulting compiler warning.

Please specify below if you have used Rust before this course:

To be able to follow step 1, you will need a language feature known as pattern matching. We have not covered it in this course due to lack of time and conflicting priorities, but here is a simple code example that should give you a good starting point:

#![allow(unused)]
fn main() {
fn option_demo(value: Option<String>) -> Option<String> {
    // Can check if an Option contains Some or None nondestructively...
    if let Some(x) = &value {
        println!("Received a string value: {x}");
    } else {
        println!("Did not receive a value");
    }

    // ...in the sense that if `&` is used as above, `x` is not a `String` but
    // a `&String` reference, and therefore the above code does not move
    // `value` away and we can still use it.
    value
}
}

Still at step 1, you will also need to know that by default, anonymous functions aka lambdas capture surrounding variables from the environment by reference, and you need to add the move keyword to force them to capture surrounding variables by value:

#![allow(unused)]
fn main() {
let s = String::from("Hello world");

// This function captures s by reference
let f_ref = || println!("{s}");
f_ref();
// ...so s can still be used after this point...

// This one captures it by value i.e. moves it...
let f_mv = move || println!("{s}");
f_mv();
// ...which means s cannot be used anymore starting here
}

At step 3, you will run into trouble with a function that returns an hdf5::Result. This result type is not general-purpose anymore, as it can only contain HDF5 errors whereas we now also need to handle Vulkan errors. Replacing this specialized HDF5 result type it with the more general grayscott_exercises::Result type will resolve the resulting compiler error.


Once you are done with the above refactoring, proceed to modify the Context::new() constructor to also support this new feature.

Then change examples/bin/simulate.rs to create a context instead of a raw instance, and adjust any other code that calls into Context::new() as needed.

While doing so, you will likely find that you need to adjust the Gray-Scott simulation CLI arguments defined at exercises/src/grayscott/options.rs, in order to let users of the simulate binary configure the Vulkan context creation process on the command line, much like they already can when running the square binary that we worked on in the previous course section.

And that will be it. For the first version of our Vulkan-based Gray-Scott reaction simulation, we are not going to need any other change to the Context and Instance setup code.


  1. …and print!(), and eprintln!()… basically any kind of textual application output over stdout and stderr will break indicatif’s progress bar rendering along with that of any other kind of live terminal ASCII art that you may think of, which is a great shame.

  2. On the Linux/macOS command line, you can find these by calling the grep logger_info command at the root of the exercises/ directory.