Using and Understanding the Code (2024)

The pbrt source code distribution is available frompbrt.org. The website also includes additionaldocumentation, images rendered with pbrt, example scenes, errata, andlinks to a bug reporting system. We encourage you to visit the websiteand subscribe to the pbrt mailing list.

pbrt is written in C++, but we have tried to make it accessible tonon-C++ experts by limiting the use of esoteric features of thelanguage. Staying close to the core language features also helps with thesystem’s portability. We make use of C++’s extensive standard librarywhenever it is applicable but will not discuss the semantics of calls to standardlibrary functions in the text. Our expectation is that the reader willconsult documentation of the standard library as necessary.

We will occasionally omit short sections of pbrt’s source code from thebook. For example, when there are a number of cases to be handled, allwith nearly identical code, we will present one case and note that the codefor the remaining cases has been omitted from the text. Default classconstructors are generally not shown, and the text also does not include detailslike thevarious #include directives at the start of each source file. All the omitted code can be found in the pbrt source codedistribution.

1.5.1 Source Code Organization

The source code used for building pbrt is under the src directoryin the pbrt distribution. In that directory are src/ext, whichhas the source code for various third-party libraries that are used bypbrt, and src/pbrt, which contains pbrt’s source code. We willnot discuss the third-party libraries’ implementations in the book.

The source files in the src/pbrt directory mostly consist ofimplementations of the various interface types. For example,shapes.h and shapes.cpp have implementations ofthe Shape interface, materials.h andmaterials.cpp have materials, and so forth. That directoryalso holds the source code for parsing pbrt’s scene description files.

The pbrt.h header file in src/pbrt is the first filethat is included by all other source files in the system. It contains afew macros and widely useful forward declarations, though we have tried tokeep it short and to minimize the number of other headers that it includesin the interests of compile time efficiency.

The src/pbrt directory also contains a number ofsubdirectories. They have the following roles:

  • base: Header files defining the interfaces for 12 of the commoninterface types listed in Table1.1(Primitive and Integrator are CPU-only and so are defined infiles in the cpu directory).
  • cmd: Source files containing the main() functions forthe executables that are built for pbrt. (Others besides the pbrt executableinclude imgtool, which performs various image processingoperations, and pbrt_test, which contains unit tests.)
  • cpu: CPU-specific code, including Integrator implementations.
  • gpu: GPU-specific source code, including functions forallocating memory and launching work on the GPU.
  • util: Lower-level utility code, most of it not specific torendering.
  • wavefront: Implementation of theWavefrontPathIntegrator, which is introduced inChapter15. This integrator runs on both CPUs and GPUs.

1.5.2 Naming Conventions

Functions and classes are generally named using Camel case, with the firstletter of each word capitalized and no delineation for spaces. Oneexception is some methods of container classes, which follow the namingconvention of the C++ standard library when they have matchingfunctionality (e.g., size() and begin() and end() foriterators). Variables also use Camel case, though with the first letterlowercase, except for a few global variables.

We also try to match mathematical notation in naming: for example, we usevariables like p for points and w for directions. We will occasionally add a p to the end of a variable todenote a primed symbol: wp for . Underscores are used toindicate subscripts in equations: theta_o for ,for example.

Our use of underscores is not perfectly consistent, however.Short variable names often omit the underscore—we use wi for and we have already seen the use of Li for .We also occasionally use an underscore to separate a word from a lowercasemathematical symbol. For example, we use Sample_f for a methodthat samples a function rather than Samplef, which would be moredifficult to read, or SampleF, which would obscure the connection tothe function (“where was the function defined?”).

1.5.3 Pointer or Reference?

C++ provides two different mechanisms for passing an object to a function or method by reference: pointers and references. If afunction argument is not intended as an output variable, either can be usedto save the expense of passing the entire structure on the stack. Theconvention in pbrt is to use a pointer when the argument will be completelychanged by the function or method, a reference when some of its internalstate will be changed but it will not be fully reinitialized, andconst references when it will not be changed at all. One importantexception to this rule is that we will always use a pointer when we want tobe able to pass nullptr to indicate that a parameter is notavailable or should not be used.

1.5.4 Abstraction versus Efficiency

One of the primary tensions when designing interfaces for software systemsis making a reasonable trade-off between abstraction and efficiency. Forexample, many programmers religiously make all data in all classesprivate and provide methods to obtain or modify the values of thedata items. For simple classes (e.g., Vector3f), we believe thatapproach needlessly hides a basic property of the implementation—that theclass holds three floating-point coordinates—that we can reasonablyexpect to never change. Of course, using no information hiding andexposing all details of all classes’ internals leads to a code maintenancenightmare, but we believe that there is nothing wrong with judiciouslyexposing basic design decisions throughout the system. For example, thefact that a Ray is represented with a point, a vector, a time, andthe medium it is in is a decision that does not need to be hidden behind alayer of abstraction. Code elsewhere is shorter and easier to understandwhen details like these are exposed.

An important thing to keep in mind when writing a software system andmaking these sorts of trade-offs is the expected final size of the system.pbrt is roughly 70,000 lines of code and it is never going to grow to bea million lines of code; this fact should be reflected in theamount of information hiding used in the system. It would be a waste ofprogrammer time (and likely a source of runtime inefficiency) to designthe interfaces to accommodate a system of a much higher level of complexity.

1.5.5 pstd

We have reimplemented a subset of the C++ standard library in thepstd namespace; this was necessary in order to use those parts of itinterchangeably on the CPU and on the GPU. For the purposes of readingpbrt’s source code, anything in pstd provides the samefunctionality with the same type and methods as the correspondingentity in std. We will therefore not document usage ofpstd in the text here.

1.5.6 Allocators

Almost all dynamic memory allocation for the objects that represent thescene in pbrt is performed using an instance of an Allocator thatis provided to the object creation methods. In pbrt, Allocator isshorthand for the C++ standard library’s pmr::polymorphic_allocatortype. Its definition is in pbrt.h so that it is available to allother source files.

<<Define

Allocator

>>=

using Allocator = pstd::pmr::polymorphic_allocator<std::byte>;

std::pmr::polymorphic_allocator implementations provide a fewmethods for allocating and freeing objects. These three are used widely inpbrt:

void *allocate_bytes(size_t nbytes, size_t alignment);template <class T> T *allocate_object(size_t n = 1);template <class T, class... Args> T *new_object(Args &&... args);

The first,allocate_bytes(),allocates the specified number of bytes of memory. Next,allocate_object()allocates an array of n objects of the specified type T,initializing each one with its default constructor. The final method,new_object(),allocates a single object of type T and calls its constructor withthe provided arguments. There are corresponding methods for freeing eachtype of allocation:deallocate_bytes(),deallocate_object(),anddelete_object().

A tricky detail related to the use of allocators with data structures fromthe C++ standard library is that a container’s allocator is fixed once itsconstructor has run. Thus, if one container is assigned to another, thetarget container’s allocator is unchanged even though all the values itstores are updated. (This is the case even with C++’s move semantics.) Therefore,it is common to see objects’ constructors in pbrt passing along anallocator in member initializer lists for containers that they store evenif they are not yet ready to set the values stored in them.

Using an explicit memory allocator rather than direct calls to newand delete has a few advantages. Not only does it make it easy todo things like track the total amount of memory that has been allocated,but it also makes it easy to substitute allocators that are optimized formany small allocations, as is useful when building acceleration structuresin Chapter7. Using allocators in this way alsomakes it easy to store the scene objects in memory that is visible to theGPU when GPU rendering is being used.

1.5.7 Dynamic Dispatch

As mentioned in Section1.3, virtual functionsare generally not used for dynamic dispatch with polymorphic types inpbrt (the main exception being the Integrators). Instead, theTaggedPointer class is used to represent a pointer to one of aspecified set of types; it includes machinery for runtime typeidentification and thence dynamic dispatch. (Its implementation can befound in AppendixB.4.4.) Two considerations motivateits use.

First, in C++, an instance of an object that inherits from an abstract baseclass includes a hidden virtual function table pointer that is used toresolve virtual function calls. On most modern systems, this pointer useseight bytes of memory. While eight bytes may not seem like much, we havefound that when rendering complex scenes with previous versions of pbrt, asubstantial amount of memory would be used just for virtual functionpointers for shapes and primitives. With the TaggedPointer class,there is no incremental storage cost for type information.

The other problem with virtual function tables is that they store functionpointers that point to executable code. Of course, that’s what they aresupposed to do, but this characteristic means that a virtual function table canbe valid for method calls from either the CPU or from the GPU, but not fromboth simultaneously, since the executable code for the different processorsis stored at different memory locations. When using the GPU for rendering,it is useful to be able to call methods from both processors, however.

For all the code that just calls methods of polymorphic objects, the use ofpbrt’s TaggedPointer in place of virtual functions makes nodifference other than the fact that method calls are made using the. operator, just as would be used for a C++ reference.Section4.5.1, which introduces Spectrum, thefirst class based on TaggedPointer that occurs in the book,has more details about how pbrt’s dynamic dispatch scheme is implemented.

1.5.8 Code Optimization

We have tried to make pbrt efficient through the use of well-chosenalgorithms rather than through local micro-optimizations, so that thesystem can be more easily understood. However, efficiency is an integralpart of rendering, and so we discuss performance issues throughout thebook.

For both CPUs and GPUs, processing performance continues to grow morequickly than the speed at which data can be loaded from main memory intothe processor. This means that waiting for values to be fetched frommemory can be a major performance limitation. The most importantoptimizations that we discuss relate to minimizing unnecessary memoryaccess and organizing algorithms and data structures in ways that lead tocoherent access patterns; paying attention to these issues can speed upprogram execution much more than reducing the total number of instructionsexecuted.

1.5.9 Debugging and Logging

Debugging a renderer can be challenging, especially in cases where theresult is correct most of the time but not always. pbrt includes anumber of facilities to ease debugging.

One of the most important is a suite of unit tests. We have found unittesting to be invaluable in the development of pbrt for the reassuranceit gives that the tested functionality is very likely to be correct.Having this assurance relieves the concern behind questions during debuggingsuch as “am I sure that the hash table that is being used here is notitself the source of my bug?” Alternatively, a failing unit test isalmost always easier to debug than an incorrect image generated by therenderer; many of the tests have been added along the way as we havedebugged pbrt. Unit tests for a file code.cpp are found incode_tests.cpp. All the unit tests are executed by an invocationof the pbrt_test executable and specific ones can be selected viacommand-line options.

There are many assertions throughout the pbrt codebase, most of them notincluded in the book text. These check conditions that should never betrue and issue an error and exit immediately if they are found to be trueat runtime. (See SectionB.3.6 for the definitions of theassertion macros used in pbrt.) A failed assertion gives a first hintabout the source of an error; like a unit test, an assertion helps focusdebugging, at least with a starting point. Some of the morecomputationally expensive assertions in pbrt are only enabled for debugbuilds; if the renderer is crashing or otherwise producing incorrectoutput, it is worthwhile to try running a debug build to see if one ofthose additional assertions fails and yields a clue.

We have also endeavored to make the execution of pbrt at a given pixelsample deterministic. One challenge with debugging a renderer is a crashthat only happens after minutes or hours of rendering computation. Withdeterministic execution, rendering can be restarted at a single pixelsample in order to more quickly return to the point of a crash.Furthermore, upon a crash pbrt will print a message such as “Renderingfailed at pixel (16, 27) sample 821. Debug with --debugstart 16,27,821”.The values printed after “debugstart” depend on the integrator beingused, but are sufficient to restart its computation close to the point of acrash.

Finally, it is often useful to print out the values stored in a datastructure during the course of debugging. We have implementedToString() methods for nearly all of pbrt’s classes. They return astd::string representation of them so that it is easy to print theirfull object state during program execution. Furthermore, pbrt’s customPrintf() and StringPrintf() functions(SectionB.3.3) automatically use the string returned byToString() for an object when a %s specifier is found in theformatting string.

1.5.10 Parallelism and Thread Safety

In pbrt (as is the case for most ray tracers), the vast majority of dataat rendering time is read only (e.g., the scene description and texture images).Much of the parsing of the scene file and creation of the scene representationin memory is done with a single thread of execution, so there are fewsynchronization issues during that phase of execution. During rendering, concurrent read access to all theread-only data by multiple threads works with no problems on both the CPUand the GPU; we only need to be concerned with situations where data inmemory is being modified.

As a general rule, the low-level classes and structures in thesystem are not thread-safe. For example, the Point3f class, which storesthree float values to represent a point in 3D space, is not safe formultiple threads to call methods that modify it at the same time.(Multiple threads can use Point3fs as read-only data simultaneously,of course.) The runtime overhead to make Point3f thread-safe wouldhave a substantial effect on performance with little benefit in return.

The same is true for classes like Vector3f, Normal3f,SampledSpectrum, Transform, Quaternion, andSurfaceInteraction. These classes are usually either created atscene construction time and then used as read-only data or allocated on thestack during rendering and used only by a single thread.

The utility classes ScratchBuffer (used for high-performancetemporary memory allocation) and RNG (pseudo-random numbergeneration) are also not safe for use by multiple threads; these classesstore state that is modified when their methods are called, and theoverhead from protecting modification to their state with mutual exclusionwould be excessive relative to the amount of computation they perform.Consequently, in code like the ImageTileIntegrator::Render() methodearlier, pbrt allocates per-thread instances of these classeson the stack.

With two exceptions, implementations of the base types listed inTable1.1 are safe for multiple threads to usesimultaneously. With a little care, it is usually straightforward toimplement new instances of these base classes so they do not modify anyshared state in their methods.

The first exceptions are the LightPreprocess() method implementations. Theseare called by the system during scene construction, and implementations ofthem generally modify shared state in their objects. Therefore,it is helpful to allow the implementer to assume that only a single threadwill call into these methods. (This is a separate issue from theconsideration that implementations of these methods that arecomputationally intensive may use ParallelFor() to parallelize theircomputation.)

The second exception is Sampler class implementations; their methods are also notexpected to be thread-safe. This is another instance where thisrequirement would impose an excessive performance and scalability impact;many threads simultaneously trying to get samples from a singleSampler would limit the system’s overall performance. Therefore, asdescribed in Section1.3.4, a uniqueSampler is created for each rendering thread usingSampler::Clone().

All stand-alone functions in pbrt are thread-safe (as long asmultiple threads do not pass pointers to the same data to them).

1.5.11 Extending the System

One of our goals in writing this book and building the pbrt system was tomake it easier for developers and researchers to experiment with new (orold!) ideas in rendering. One of the great joys in computer graphics iswriting new software that makes a new image; even small changes to thesystem can be fun to experiment with. The exercises throughout the booksuggest many changes to make to the system, ranging from small tweaks tomajor open-ended research projects. SectionC.4 inAppendixC has more information about the mechanics of adding newimplementations of the interfaces listed inTable1.1.

1.5.12 Bugs

Although we made every effort to make pbrt as correct aspossible through extensive testing, it is inevitable that some bugs arestill present.

If you believe you have found a bug in the system, please do the following:

  1. Reproduce the bug with an unmodified copy of the latest version of pbrt.
  2. Check the online discussion forum and the bug-tracking system atpbrt.org. Your issue may be a known bug, or it may be acommonly misunderstood feature.
  3. Try to find the simplest possible test case that demonstrates thebug. Many bugs can be demonstrated by scene description files that arejust a few lines long, and debugging is much easier with a simple scenethan a complex one.
  4. Submit a detailed bug report using our online bug-tracking system.Make sure that you include the scene file that demonstrates the bugand a detailed description of why you think pbrt is not behavingcorrectly with the scene. If you can provide a patch that fixesthe bug, all the better!

We will periodically update the pbrt source code repository with bugfixes and minor enhancements. (Be aware that we often let bug reportsaccumulate for a few months before going through them; do not take this asan indication that we do not value them!) However, we will not make majorchanges to the pbrt source code so that it does not diverge from thesystem described here in the book.

Using and Understanding the Code (2024)
Top Articles
Latest Posts
Recommended Articles
Article information

Author: Aron Pacocha

Last Updated:

Views: 6157

Rating: 4.8 / 5 (48 voted)

Reviews: 95% of readers found this page helpful

Author information

Name: Aron Pacocha

Birthday: 1999-08-12

Address: 3808 Moen Corner, Gorczanyport, FL 67364-2074

Phone: +393457723392

Job: Retail Consultant

Hobby: Jewelry making, Cooking, Gaming, Reading, Juggling, Cabaret, Origami

Introduction: My name is Aron Pacocha, I am a happy, tasty, innocent, proud, talented, courageous, magnificent person who loves writing and wants to share my knowledge and understanding with you.