The Stop command will be handled specially by the Dispatcher to act as a
sort of broadcast into the thread pool. Instead of asking the caller to
feed in the appropriate number of stop commands, a single one will queue
up a stop for all threads. Important for the ergonomics of having a
variable number of threads (instead of the earlier magic constant).
Moreover, it's necessary if the dispatcher stops using the round-robin
style assignment.
The pool size is specifiable as an argument to Dispatcher::new().
As results come from the dispatcher('s return channel) they are pushed
into a vector to be reordered. They're sorted in reverse-order so that
they can be popped from the vector. Upon receipt and buffering of a
scanline, a loop checks the tail of the buffer to see if it's the
next-to-write element. Since the tail is popped off, this loop can run
until this condition is not met.
Saving for reference more than anything. The threads take ownership of
the data (the closures do, but whatever). Moving the return channel out
of the dispatcher means the dispatcher can't be moved into the feeder
thread.
I see a few solutions from here:
1. Proxy the return channel with another channel. Give the whole
dispatcher to the feeder thread and hook up another output channel.
Have the feeder unload the return and pass it through.
2. Rewrite the dispatcher constructor to pass a tuple of the dispatcher
minus it's return channel, and the return channel now as a separate
object. This could let them have independent lifetimes and then I can
pass them around like I'm trying to do.
3. Have main do all the job feeding, result unloading, and
recompositing. Don't have a feeder and collector thread, and just
have main bounce between loading a few, and unloading a few.
The job dispatcher has been hooked up and four threads are rendering the
scene. There's a super important caveat, though: The job submission is
blocking and will prevent the main thread from continuing to the result
collection. The threads will be unable to contiinue without having
output space, though. Since the buffers are only size 1, this will cause
4 scanlines to be rendered, and then nothing else to be done. Deadlock.
Bumping the input buffer to 100 lets the submission loop fill up the
workload and then move on to collecting.
There's also no scanline sorting, so everything gets <<wiggly>>.
It's getting fiddly and awful keeping all the thread control pieces all
over the place. Existing ThreadPool implementations seem a little weird,
and this is a learning experience. I'll just make my own!
The dispatcher constructor only creates the threads and sets up their IO
channels. It has another method for serial submission of jobs.
The job allocation follows a round-robin selection, but I have some
concerns about starvation doing this -- consider a long running scanline
render. It would make the submission call block and other threads could
become available for that job. But they'll be ignored, since the
round-robin assignment doesn't have any skip mechanisms.
There are now two threads: The main thread (obv) and a single worker
thread to render lines. There's a render context struct to keep track of
the many, many arguments to the `render_line()` function. Jobs are
submitted through a channel to the thread, and results are returned
through another. Next up is to make a bunch of threads and collect
across them.
I hit an issue implementing the threading where the use of trait objects
got real angry about lifetimes. The variants of geometry could be
described as a runtime variable (thus requiring dynamic dispatch), but
I'm not gonna do that. Instead: Enums!
I had been carrying the distributions around with the SmallRng object so
that I can provide whatever bounds needed. It turns out that the book
only takes advantage of this a couple of times.
The functions accepting a Uniform struct no longer do. They instead
construct their own temporary one -- hopefully the optimizer can see
through the loops and construct it just once. :l
The change exposed all the uses of the distributions (duh!) and now they
are correct. The effect is most pronounced on the dielectric spheres.
Final render! The world is pseudorandomly generated. The glass ball in
the middle doens't come out quite right, though. I think there are bad
random ranges being passed around. Things are using (-1,1) when they
should be [0,1). Bug fix incoming.
I found the fucker. The rays were bouncing incorrectly because I wasn't
holding a value the way I should have. I thought I was being clever and
using Rusts's ability to redeclare variables. But, no. That value is
meant to be modified when the first conditional passes. The outcomes are
3 possibilities, where one is an early return. Shadowing the value that
way meant I was giving back garbage.
I don't know why this didn't have any obviously bad effects prior to
making the dielectric material.
The Vec3::as_unit() function accepts input by reference. It passes back
out a copy, however, and some inputs are even temporaries. I bet the
optimizer can see through this game and will do the right thing if I
give it a value instead. It should perform copy elision as appropriate,
even if I'm missing out on Rust's move semantics here.
Remove some old, unused, and unuseable functions.
Dielectric material matching Raytracing in a Weekend book chapter 10.2.
But it isn't right. The spheres turn into solid black holes.
As I'm making the commit, I've found the problem. I'm separating it out
so I can tag it and explore the code a bit more. See the step-by-step
changes, and such.
Segfaults and stack overflows. I get overflows when running in either
debug or release. Segmentation faults when hooked up to GDB. I think it
may be related to the copying of uninitialized trait objects, but I
don't know.
I've decided the trait-based interface just isn't worth this. I'll
rearrange the materials into an Enum and a big match block.
I get lazy with commits when following guided material. There's a design
challenge to approach, now. I'm saving here so I can revert changes in
case it goes sideways.
Materials are proving to be a little complicated in Rust semantics. The
Ray Tracing in a Weekend book uses shared_ptr's, but Rust doesn't really
like that. I'm doing references with lifetime annotations. Hopefully I
can get that the right way around to work out.
The materials themselves look like reasonable candidates for describing
as Enums. This takes away the ability to add new ones by simply impl'ing
a trait, but that was never gonna happen anyway. The code would be
modified and recompiled. There's no difference in maintenance cost if
that's a new struct impl'ing a trait, or adding enum members.