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:
- Add a new
Option<ProgressBar>
parameter to thelogger_info()
function inexercises/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 aprogress_bar.suspend(move || { /* ... logging goes here ... */ })
callback.
- If this parameter is
- 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 passNone
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.
- Finally, modify
examples/bin/simulate.rs
so that it sets up aLoggingInstance
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.
-
…and
print!()
, andeprintln!()
… basically any kind of textual application output overstdout
andstderr
will breakindicatif
’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. ↩ -
On the Linux/macOS command line, you can find these by calling the
grep logger_info
command at the root of theexercises/
directory. ↩