Dynamic Constant Buffer [C++ 3D DirectX Tutorial]

Video Statistics and Information

Video
Captions Word Cloud
Reddit Comments
Captions
you guys are both chilly welcome back to hardware 3d today's topic is a dynamic constant buffer now before we get started look down at the bottom of the video you're gonna see a number down there I don't know what the number is what's gonna be pretty big that's the number that represents the length of this video it's gonna be a long boy I just started recording I don't know how long it's gonna be but I'm sure it is going to be long maybe it's gonna be like an hour long or so and I know not all of you guys are gonna be interested in the software engineering side of things you're more just interested in you know show me the direct3d show me the algorithms or show me the API so what I've done with this video is I've structured it I've put all the essential information right at the beginning of the video so for those of you who aren't interested in you know software engineering or C++ metaprogramming just watch the first part the very first part all I'm gonna show you is the basic interface how to use the system that we build in this video and if you know this you won't get lost in future videos that are about things you are interested in like for example real-time shadows character animation alright so for those of you who aren't interested in the whole deal you can just stick around to the very first part and you'll get an idea of how the system works and you won't get lost in future videos for those of you who are more interested in that software engineering stuff stick around for part two I'm gonna talk about the motivation the reason why I create the system the problems that it solves its advantages and I'm gonna talk about just the general overall design and system how its put together all the components then if you're really interested in all the details stick around for part three and I'm gonna I'm gonna walk through the code I'm not gonna tediously explain every single line of code but I'm gonna hit all the main points and I'm gonna focus on some interesting techniques that I use all right let's jump into it here he is the repo here this commit is the beginning of the changes for the dynamic constant buffer and here is the end we're gonna be spending most of our time just looking at the finished product now if we check out the code at that commit there's one important file that I've added called testing that CPP so as I was building this system as I was adding features I wrote tests for them to validate that they actually do what they're supposed to do and instead of just writing some throwaway code and then deleting it afterwards I decided to keep it in this file here it can be reused if I ever make changes I can validate that all the old tests still worked it's the basics of automated testing right so in this file let's let me just show you a very simple example of how to use dynamic constant buffer so you include dynamic constant H all the stuff lives in the namespace DCB for dynamic constant buffer first thing you got to do it's a data structure it has a layout so you have to describe the layout before you create a buffer of that type so we do that at runtime first we create a raw layout and we'll just call this one leh that's just an empty structure now we have to add some members to it so let's go lay bad and we want to add DCB and there's a bunch of types you can add basically stuff that you would see in an HLSL constant buffer so like float float to float three full of four just your standard vector types there's also matrix and bool let's add a float keep it simple right start off with here and we'll call it floaty that sounds like a good name all right so now we've described the layout of our data structure let's actually make that bad boy we'll go Auto buff is equal to DCB buffer and we gotta give it the layout so we can still move that in don't waste any resources don't make any unnecessary copies so move our layout into here that creates the buffer now let's use the buffer let's access some data let's right into our buffer so we go buff now we want to access the member floaties you just got to pass that string in there floating that's the key and there's a lot of meta programming mechanisms that go on under the hood that allow us just to assign a floating value right in there with a simple assignment and it just works I mean it looks like code that you might see in a more dynamic language like Python or JavaScript but it's C++ it's almost as if we've extended the language itself here that's the whole idea of meta programming right and it's gonna be very intuitive very easy to read design and I feel like it's going to actually make the the future tutorials easier so for those of you even aren't interested in this meta programming stuff you're still gonna gain benefits because it's gonna make the future videos on topics you are interested in easier to digest but anyway so we store a value into our dynamic constant buffer now let's let's verify that was properly stored so we'll go float F is equal to buff at float how many of you guys see the problem here so you've probably caught on but we created it with a member called floaties but we're accessing it as float now because this is dynamic at runtime the compiler can't catch this but we're still gonna get Diagnostics when I try to access float it's gonna throw an assertion it's going to say tried to resolve you can click retry and you can go up the call stack and you could figure out where where went wrong and we can see ID went wrong right here so it's still possible to catch those I've put in lots of assertions and stuff like that so it will catch your mistakes it won't just silently fail and cause weirdness that's hard to debug so let's give this the correct names now we're step-step-step we're gonna store in two floaties it worked and then we're gonna load back out and it should give us a 1 and it does given us a 1 weave round-trip to our data and it's integrity is intact beautiful and I'm sure you'll agree this syntax is pretty easy to understand all right let's spice things up a little bit we're gonna put two members one float three and one float - and we're gonna assign to them so we set up the layout and we build our buffer now the way the buffer is set up talk about this more in part two but there are two main parts to it there's the actual bytes just a raw continuous byte buffer of the actual values and then there is a layout and this is a tree structure that describes the it's the map of those bytes of this buffer right because the buffer key is a struct can have multiple members some of those members can be struct with members inside of themselves and you can also have a raised arrays of structs so it can get quite complicated you need a map of that landscape in order to figure out where the bytes are for a given you know key name or path so we can expand this by 4 bytes we can see that they're all set to 0 to begin with now when I write to floatie which is the first member here you're gonna see what you expect you're gonna see you know 4 bytes per float 3 floats so you can see the first 12 bytes are being sent to various values okay now what happens when we try to write to flow to flow to tea you're gonna see these bytes are being saying you may be saying well what's what's this hole here well that's one of the beauties of this system it automatically adds padding so that the layout of this buffer of bytes exactly matches the layout of an identical buffer on HLSL side so you don't have to worry about alignment and packing and all the rules for HLSL that's all automatically taken care of for you once you have this buffer you can just copy those bytes directly over to the GPU and they will be aligned properly for access from your shaders like I said there's many checks that go into here that will aid you in creating code that doesn't have bugs so stuff like adding a duplicate key into a data structure here I'm gonna throw an exception right adding duplicate name destruct similarly if I try to write the wrong kind of data to this member here you're gonna get another exception right so it's dynamic but it's also type safe at least when you're running in debug when you're running a release a lot of those checks are removed to improve performance now what's interesting is because it's dynamic and runtime it can't perform that check you can't tell whether float 3 is the right type for this until it actually runs the code however if you try to assign it a value that just isn't in the system at all like let's see I tried to assign to it a string we got some metaprogramming in there it'll tell you right now unsupported sis type used in assignment and if you click here it shows you exactly where the problem was so there's even some support for compiler checking of your expressions where it's possible by the way if you're interested in what kinds of types are supported in the dynamic constant buffer you've got float float two three four matrix and bool and I'll probably add integer and maybe some other ones in the future but you can go to dynamic constant ith look at the very top and you'll see the list of supported datatypes now as I've alluded to you can have nested structures so how do we do that what's the same idea it's just another member of our base structure so we're gonna add DC be struck makes sense right and it's a member you got it's got to give it a name so we'll go struct e that's a good name right and now we got the structure but we have to give it some members you can't just have an empty structure doesn't make any sense so how do you access how do you add members to a member well you just again you just key into our layout so we'll do struct e dot add it we'll just add a float here and you might be saying chilly then doesn't these names overlap no because they're in different structures right this structure is inside of this one it's not the same level there isn't any ambiguity there isn't any overlap in this situation and then again to access that it's pretty simple key into the buffer to access the nested structure and then you key into that structure to access the leaf member the float you can set that up and then here we can load that value alright let's test it out so we had a float to our nested structure create the buffer let's set that and if we load it for 20 okay round-trip no problem now the final type of member you can add second aggregate type is an array so you can add an array and let's like everything else you give it a name we'll call it our migi and what you were saying well we've added this array but we don't what type it is we don't know how many elements yeah so you actually need a two-step process to set up an array first you add then when you have the array in there you have to set its type and we'll just say the type is DCB float and the number of elements will give it ten elements the access again is what you would expect when indexing into an array instead of using a string you're gonna use an integer and F three here we can see yeah we round-trip our data no problem all right now let me show you the most complex scenario we have which is adding an array of structures so the first part is simple enough you add the array you set its type as a structure so I have 69 of them in the array but now how do you actually define the the structure the shape of this structure that's in the array well that is the tricky part you go lay this should be r2 we go r2 dot T and that access is the type of the array and once we have the type then we can add to it because the type is a structure we can add to that structure so let's add a float three and we'll also add a single float in here and here's what that access would look like we run it and we see yeah we get no problems expected result right here so there you go there's the most complicated scenario and you can nest this however deep you want it's completely free it's completely dynamic you can have a structure of structures that has an array of structures and that structure has a bunch of structures and some of those have arrays it's Turtles all the way down my friend no I know it's quickly at some of the dynamic reflection you can do on these things so let's say I only want to write to a value if it exists in the structure I don't want to try to write to it if it doesn't exist that's gonna cause a big problem so how do I do that well we can do auto ref is equal to and then we'll index into our buffer so I'll talk a little bit more about this in part two but what this is doing is it's not just it's not returning a dumb reference to you know an XM float - it's actually returning a smart proxy odd and what we can do is we can check that proxy object to see if it's actually a valid key in the data structure bingo ref that exists so we do if ref not exists then we can write to that part of the buffer so here we key into the structure make sure it exists and it does exist so we write to the structure what happens if we try a key that doesn't exist it's gonna try to key into the structure but since this key doesn't exist the reference when we call exists on it it's going to return false because it is what we have is what I call an empty reference so this allows you to speculative really try to access members of a structure and if they don't exist no no harm no foul I also have this helper here set if exists and that will sit if it exists or if not it'll do nothing it's also possible to get a pointer to some bytes in the buffer that looks like this and then we could use that pointer to directly set those bytes so we get the pointer if we look at P yeah it says it's pointing to something that has a value of 69 and if we set that now at 69 point 6 96 96 9 and if we look into the buffer the very end of the bytes here yeah we got our four bytes from that setting finally the buffer supports cons correctness so if I do a Const Auto reference we'll call it C ref and initialize that as a reference to the buffer you can see here if I try to get a non-constant err won't let me I try to do an assignment it's not gonna let me but it will still let me read from the data structure no problem all right that's all pretty much all the important syntax was bad boy let's take a look at some places where it's actually used to wrap up by section one in this video so if you look at mesh dot cpp we can see now for the constant buffers I mean obviously I'm going to be using my new layout system so you create a raw layout you add all the members that are required create the buffer initialize those members to whatever values you want to have them in and then you create your bindable it's that simple and we have to create a new bindable right is this now it's not the old one was templated statically templated on a structure and this new one is going to take in the dynamic constant buffer structure type so we look at constant buffers e^x right now what I'm doing is I've only got implementation for dynamic constant buffer for pixel constant buffers because vertex I haven't had much need for it up until now probably get to it eventually but not right now and there's two main versions there is a caching version and that stores the buffer right in this structure on the CPU side it keeps a copy of the buffer contents on the CPU side non caching version does not keep a coffee copy on the CPU side does only update it loads that those values over to the GPU but it doesn't maintain a shadow copy on the CPU side that's the main difference between these two and they work basically the way you would expect this this is the main constructor that you would use and you pass it graphics you pass it the buffer you pass it what's slot that that is constant buffer should bind to and it'll create that stuff for you there's also an alternative version that takes a layout instead of a buffer so you can tell it to create a new empty buffer you're not giving it any data and just tell it what the layout of that buffer should be but normally you're gonna use this one right here you're gonna create your buffer and then you're gonna create your caching pixel constant buffer X from your dynamic constant buffer that's it that's part one out of the way hopefully when I edit this down it won't be too long and that should be the minimal that you guys need get an idea of how this code works so when you see it in future videos on other topics it's not gonna throw a monkey wrench into your understanding you'll be able to follow no problem I anticipate that actually having the system in here is gonna make the other videos more streamlined so to make the process a lot better for me cuz it's gonna be easier to develop stuff with custom buffers there's more error checking I don't have to figure out the alignments and stuff so if you don't like anything C++ you're not interested in software engineering aspect architecture aspect to this you just wanted these videos for the API and the graphic stuff you can peace out see you guys later no harm no foul but for the of you who are interested we're gonna move on to the motivation for this design and I give a little bit of overview of what the design looks like now let's talk about the motivation behind the system and what it gains us so with this we have dynamic composition of the data structures right we can configure them dynamically at runtime in response to information that we may only be able to get at a runtime and that's pretty powerful we take a look at the system before I implemented all the dynamic constant buffer stuff you know look at mesh stats CPP this big ass chain of if else's the main reason why this exists is because we need a different static constant buffer structure for every different configuration of resources that a mesh has right now if we could compose this dynamically at runtime if we could just for example if we detect that this mesh has a diffuse map if we could just add in the things that I diffuse map needs and build them all up we wouldn't need this big long if chain where we duplicate a bunch of code for every possible combination of resource types and some time after the dynamic con stuff I actually do that so let's look at my current code what do I have right now so this is the future code a little bit of a sneak peek at what its gonna look like in the future I have a file here material that's CPP and for the Phong technique to build up all the stuff in the constant buffer and in the vertex layouts you just check for each thing we're gonna check God do we have a diffuse texture if so we're gonna append texture 2d to our vertex layout now if we don't have a diffuse texture we're gonna add a constant color to our constant buffer and the same idea for specular from normal Maps and here's there's some common stuff for all of them so this handles all of the cases and it's maybe what it's less than a hundred lines of code here now again compare this to the old system starts from line 380 and it goes and it goes and it goes all the way down to 730 350 lines of code versus about a hundred lines of code which would you prefer so that's one benefit the ability to dynamically compose our structure at runtime the other thing is that we can dynamically query the structure we can have our code dynamically read the layout of our constant buffer and that's called reflection let me take a look at mesh dot H remember the whole thing we had here where we had the ability to control different meshes but since meshes are polymorphic different meshes are gonna have different constant buffers but they are all statically templated and it was a nightmare to figure out which is which we're only supporting two different kinds of constant buffers here two different kinds of meshes and even with this we've got this on sticks pression if and then we've got a dynamic if inside of that and we have to do that for every single one that we want to support and then mesh dot cpp here we basically got a call control me daddy' with every single different kind of static buffer struct that we want to support we call with the first one if it fails then we try calling with the second one and if that fails we'd have to do the third one the fourth one the fifth one it's just with the new dynamic constant buffer system we can just query does it have a normal map if so provide a checkbox for that normal map enabled control does it have a member called specular map enabled put a checkbox for that and then any kind of constant buffer doesn't matter if you have three types of constant buffers or 300,000 types of constant buffers if it has that member you can support it with the appropriate control you can take the appropriate action all dynamic it's beautiful it's poetry in motion my friends that's not all you get because in the future we're gonna do something called shader reflection where we can reflect on our shaders we can read the contents of a shaders what kind of constant buffers they have what are the members of the constant buffers what are the parameters the shaders take we can read all that information at runtime and then we can use that information to build our dynamic custom buffer on the CPU side so we will never it will always match and it will always react to changes to the yeah the shader code and we can realize a single source of truth and that's an amazing thing to have even before we do that we could also just die for defining one a constant buffer on the CPU side we can use reflection to just do a check to make sure it matches what it's trying to load on the HLSL side and there's more because as I've shown you our system here it automatically calculates padding for us and that's that can be a huge source of human error that can waste hours of debugging time over the course of a project so this system does that that does that annoying boilerplate layout for you automatically and without fail is that enough motivation I think it is moving on let's take a look at the actual design of the system now if you watch my video on the dynamic vertex buffer and I hope you have a lot of this is probably going to seem familiar to you it is same but different so we can conceptualize it like this buffer has two parts to it I've already shown you this in the first section of this video but you've got the layout and you've got the actual bytes the bytes is just just the raw bytes of the data these bytes can be directly copied over to the GPU and you're good to go now the layout is what tells you what makes that connection when you when you try to key into a structure or index into an array you're going to be navigating the layout in order to arrive at some offset into these bytes so provides the map of the landscape of this just dumb buffer of bytes and at its heart it is a it's a tree structure right because structs can they can have members just plain data members like bull or float for but they can also have other structs and then those trucks can have struts so conceptually it's a tree and in practicality in the actual implementation it's also a tree every element of the layout here stores the offset into its position in the bytes here you use the keys to navigate this tree to arrive at a leaf node that gets you an offset and you can use that to read and write to and from this buffer of bytes seems easy right well maybe not that simple there's actually quite a few objects that go into building this system dynamic constant buffer isn't just one or two classes it's a whole ecosystem of classes that work in concert to provide that syntax and to provide all that type safety and correctness checking you should be familiar with to in here right away because you've seen them in section one that's buffer and rah layout and these are the two forward-facing classes that you're going to be interacting with the most when you use this system all this other stuff is kind of just behind the scenes now before we talk about that behind the scenes stuff let's just quickly go over the notation that I'm using here it's not the diagram makes a little more sense you can see that there are different colored lines the blue lines mean referencing by reference or pointer or they mean ownership usually by smart pointer so for example a layout object is just a shell that contains the the tree of layout elements so a layout is composed of one or more layout elements in a tree structure likewise a buffer also has layout elements because it needs a layout in order to describe the configuration of the bytes in its raw buffer you're probably familiar with the white lines this is just inheritance right so for example for a layout this is the base type but there's a raw layout that's the one that you use that you can you know add members to build it up configure it and then there's a cooked layout a cooked layout cannot be changed the third kind of line here with the circles I don't know what to call this one maybe I call this one interface maybe I call it pooping because and you'll be familiar with this if you've watched my video on the dynamic vertex buffer but now when you want to access members with the data of a buffer you index into it or you key into it and what that's gonna return is that's gonna return a proxy type that is a proxy to the bytes the buffer and that proxy type will implement interfaces for assignment operator so if you try to assign to it with a float it will try to write floats into the correct bytes in the buffer so these yellow ones mean yeah when you index into it or when you do a conversion or assignment it's going to try possibly to poop something out that will handle that so yeah these yellow lines they mean that you know what when you try to hinder X into something it's gonna poop out a proxy type that will represent that thing that you index to well the yellow lines can also mean that you know if you try to assign to this proxy type there's a operator for that that is going to take that assignment and use it to modify the bytes in this buffer so that's the basic notation let's get on with the system so what do we have here well let's begin at the beginning you know you start off you create a row layout which is a kind of layout it will have a route layout element that's a struck when you index into the layout it's gonna poop out a reference to your route struct and then you can do things like call add on that struct and in doing this you can build up a tree of layout elements now every layout element it stores the type whether it's a float or whether it's a matrix bool those are leaf types or whether it's a struct or an array now for the leaf types all you need to store is the type which is an enum and the offset that's all you need but for struct and array you've also got to store extra types right because a struct it's members are also layout elements so a struct is going to have a vector of layout elements and you can have so if you have a layout element that's a struct it's gonna have a vector of other layout elements and you know maybe this one is a struct so it's also gonna have a vector of other layout elements you get the idea now an array layout element it doesn't have extra data as a vector of layout elements because an array only has a single type right you can have an array of float we can have an array of matrices one type so it's going to store just eight single layout element so because you are storing extra layout elements for the special layout types of struct and array we have extra data that is optionally owned by the layout element depending on what type it is you'll see this in the code later on but yeah you're gonna have a tree of these that's this thing right here that's gonna be owned by raw layout then when you want to create a buffer you're gonna pass the raw layout to the buffer the buffer is going to cook the layout that's important by the way because when you build up this structure the offsets aren't fixed because it's still changeable you could still add members in between stuff like that so what happens is you build up this raw layout it determines the tree structure but the offsets aren't determined until you cook the raw layout so when you pass it to the buffer the raw layout gets cooked you have a finalized layout from that finalized layout do you know how many bytes you need so then the buffer can create its array of bytes and it has its layout and it's all good to go now once you have a buffer you're gonna want to start accessing the data in that buffer you know you declared a layout maybe it was a simple struct with three floats you want to access those floats so you access them by their name you use the the index operator right you key into the buffer the buffer navigates it's layout elements finds the matching one and what it does is it returns a reference element reference and what this element reference is I call this a proxy type it maintains this diagram says that the element reference maintains a reference to the buffer that's not exactly true it actually maintains a pointer directly to the byte array of the buffer so it has a pointer to the raw data and it also has a reference to the layout specifically let's say we've got a tree like this if I index into the buffer with a element ref is going to point to this layout element and it's gonna point to the byte buffer and then if I try to assign a value it's gonna look element a it's gonna get the offset it's also gonna check to see if the types match if this is actually you know if I'm trying to assign a float it's gonna check to make sure that this is a float and if it is gets the offset and writes into the proper place in these bytes so that's a simple scenario of you know this line of code here so what about this line of code here now we're indexing or keying into a path B and then D so what happens well first we index into the buffer B it looks through or basically asks this route destruct here do you got a B and it says yeah so it gets that be it returns you an element ref that points to this B and that points to the data then you index into this B with the key D so again now you're calling this one element ref checks its B it says do you get a D oh yeah so it poops out another element ref this element revving to D buff poops out an element ref which poops out another element ref and this element ref points to D now we want to assign does a type checked it says is D a float sure so using the offset of D which let's say is here it writes the correct bytes into the raw byte buffer and that's how it works in a nutshell now I mean element ref acts as a reference logical reference to some element in this buffer here what if I wanted a pointer to these bytes in the buffer well you might say hey I'll just do you know address of buff at you know some key well what is that gonna do this returns an element ref it poops out an element ref we're gonna get the address of this proxy object which is not going to be the address of these bytes in here so that's no good so if we want to be able to get pointers to these bytes we have to implement our own address of operator for element ref so that's what we do when we do address of on an element ref it is going to poop out a proxy type is similar to element Ref except that instead of converting to a reference to tee it converts to a pointer to tee and then you can use that you can store that pointer and use that to directly manipulate the bytes in this buffer so this is just a little bit of a helper to make life easier to make the syntax sexier not strictly necessary and down here we have constant element ref and it has its own pointer type and these guys similar to element ref they allow you no indexing or keying into the data structure they allow conversion but they don't allow assignment and the conversion is only to a Const reference so if you have a Const buffer and you index into it it is gonna pop out a Const element ref which will not allow you to modify anything in these bytes only to read them so this this enables Const correctness for when you want it that's it that's basically the overview of the system design here it does a lot of things got a lot of neat features there are many many more features that I had gone through my mind that I had considered that I might add later on I've got a big list I got a big wish list but you can't just keep building things and keep adding features to something like this you'll never get your main project done so I built what I needed and I left the rest as maybe one day if I find a real need for it there are many things that I know could be added that this net would be neat believe me I'm aware of it one of the things that I would I'm very likely to add is the ability to iterate over the tree of layout elements as part of reflection because right now the only reflection you can do is you can check to see whether a particular key exists I thought that reminds me one more thing so let's say you've got a layout tree two elements a and B we know if we index in with a it's gonna return it's gonna poop out an element ref that maintains a reference to this layout element but what happens if we try to index in with an element key that doesn't exist with a seat well what this does actually is that there is a special kind of layout element the type is empty so if you try to index into a struct and that key doesn't exist it doesn't fail it doesn't throw an exception it doesn't bug out all it does is it returns a poops out and element Ref but this one it's pointer points to a single empty layout element this is just this just exists statically it's a global basically so when you have an empty one now if you try to assign to it if you try to convert that will throw an error but you can call the function empty and it will return true for an empty element ref an element ref that points to an empty layout element or I should say to the empty layout element and this is what is enabling the basic runtime reflection that we have right now like I said in the future I will have a way of iterating over the entire tree and you know asking every layout element its name or whatever name and type but for right now we can just query by checking empty or not but yeah that's the basic of the system the overview of the system architecture and design and I believe this will be part two taken care of all right now the fun part let's actually dive into the implementation code dun dun it's very scary but but don't worry I'll be here we'll get through this together let's hold hands so dynamic constant RH dynamic constant dot CPP these are the implementation files everything for dynamic constant is inside the namespace TCB except for this macro which obviously doesn't work in namespaces but this macro gets undefined at the end of this age file so it doesn't pollute anything else anyways so quick overview we have an X macro list of all the other types we want to support I'll talk about that in a little bit and then here we have a lookup map for attributes about every one of these types this is for compile time lookup via templates and you've seen me use this in the dynamic cover text buffer system but I'll talk again very quickly about it reverse lookup here are the actual classes that's what you saw in the diagram we got the layout element we have layout which is the base and then raw layout and cook layout inherit from it then we have Const element ref element ref and then we have the buffer as you can see I have filled this source file the H file with a buttload of comments which I don't normally do but I figured for people who are really interested in this if they actually want to read the code I'll add some words in here a little bit of a side commentary by Chile might help you read through it a little easier I don't know well let's take it from the top so this is an X macro list what it allows us to do is it allows us to define the list of these different leaf element types the other types of what is it where is it layout elements that we want to support the actual raw data types you know excluding struct an array so we can define those types their names in one place and then this can be replicated in a bunch of different places all throughout our code so normally if we had bunch of switches that were switching on these names and we do if we added one we'd have to also add to every one of those switches but with an X macro it will automatically that all that other code will react when we add a new name to this list or when we remove a name from this list so how does it do that well we can see here normally with a macro you would define you know the signature of the macro and then what it expands to but with this one it's a little different we are actually invoking we're defining a list of invocations of a macro called X but there is no X macro so how does this work well what happens is when we type leaf element types it is going to invoke this X macro on every one of our types and what we can do is before we invoke this macro we define what we want X to be and that will allow us to replicate these names with some transformation so for example we have an enumeration this is all the different types of the layout element just a simple enum you say we got struct array and empty in here but for the actual data types we're using X macro to expand this and now we're doing is we're getting these names plus a comma at the end because that's what a number like so we just expand all these names and add a comment to the end that's what this X macro does we define the transformation for X that will be applied to each one of these then we invoke the X macro and then we remove the the transformation because we don't need it anymore that's how it works and now one thing that you can't use the X macro for is for our our attribute map right so where we've got a template here you've seen me use this again in dynamic vertex but we're templating on this enum and then for we do template specialization so when the template parameter is float then we have all these constic Spurs that we can read into in our in our code and you can see here I'm just defining the type assist type so when the layout type is float on our system that's just a float but when it's float to on our system that is a direct X X M float to also the size of that type on the GPU which is we need to know that for alignment purposes and for packing purposes usually it's just the same size as on the CPU except for with a bull on the CPU side bool is one byte but on the GPU side is four bytes so we can't use size of this type we've got to manually specify it as for code here is 4 this is for the signature I'll talk about that in a little bit and then valid true well if we pass in these specializations they're all valid but if we were to try to specialize this map on let's say string it would not resolve the one of these specializations it would resolve to the generic one which has set valid equal to false and there we can detect whether a specific type is valid in our system it's not really likely that one wouldn't be valid but for example if we tried to map struct which is a member of this enumeration to an attribute map it wouldn't work right because the struct isn't an actual data type it's a it's an aggregate so that's what this is for it allow us to detect bad situations like that and actually I use it here this is a neat thing because like I said with the X macros this list is very reactive we add a member in here and everything automatically changes except for the attribute map you have to add it manually so when you add a new type you have to add it here and here but what if you forget to add it here that's a problem right so what this X macro does is it expands to a static assertion for everything for every one of these guys in this list it checks to make sure that there also exists one in here so what it does the map of that type and it checks to make sure it's valid if not it prints out a nice message this message will come out in compile-time and will help us and we'll say Oh duh we added one in here we forgot to add its attributes let's do that now very nice similarly we use X macros here to divide to find a reverse map so every one of these enumeration values these ones here they each have a corresponding system like float or XM float - now with these maps we can go from your numeration to the system but what if we have the system and we want to know which enumeration it's for well we define a reverse mapping using our x macro and it will expand out this template specialization for every one of the types so for you know float float to float 3 and then we use a map from that to get the system type so now we have a struct a reverse map that's templated on system type and we can use that to get the enumeration type and again we have the valid check this one defaults to valid false so we can check to see if a specific system type is mapable in our system at compile time so there you have it there you can already see the power of the X template system it means you don't have to change you know if you change you want to add one thing to the system you don't have to change 50 places in code you have to change exactly two places and if you forget one of those places this is going to tell you so it's very clean very safe all right now let's dig into let's jump into the layout element this is the heart of the whole system this is the the cornerstone the building blocks of the layout tree right all right so right away we've got this struct here it's got a virtual destructor so we know we got some polymorphism it's called extra data base and this is the base class that the two extra data classes will inherit the extra data class for struct an array we'll get into that a little but here is where the base is declared at now we got some friend decorations got lots of friend decorations because it's a whole ecosystem of components that work together and that need I make it so that the objects within the ecosystem can access their own members for convenience for building the ecosystem but I still want the outside world do not be able to access a lot of that functionality because they only need the functionality of you know declaring the layout structure and then accessing the buffer all that other stuff they should not really be allowed to access that's just extra noise that is gonna add to the the cognitive load of anyone who's trying to use the library so I make many things private but then I make friends able to access those things within this ecosystem alright functions get signature so a layout tree which just describes a layout of a structure it has a uniquely identifying signature we can use that signature to for example store a codex of layouts and to share layouts which is actually what I do so get signature is a recursive function that recursively builds a signature actually let me let me just show you guys that right now here's that code from the testing so you can see this code here builds a fairly complicated structure it then creates a buffer from that and then we can get the route layout element of our buffer and then get the signature of that which is the signature of the entire layout right so let's do that step over this and let's look at sig mmm we can expand it here so this is the signature of that entire structure and this uniquely identifies this theoretically we could also use this as a descriptor to reconstruct a dynamic structure I haven't implemented that yet but it's something that might be possible in the future we could serialize the layout and then we could read the layout from a file it's something that is highly possible but I will implement only when I have a good use case for it again yeah geenie so that's what gets signature does exists you've seen it it checks if the element is real so well let's let's first take a look at the data getting ahead of ourselves every layout element has three members it has the offset into the byte array and that's optional by the way because offset is only valid after you have finalized the the data structure so I've got the offset we have the type which defaults to empty by the way and then we have a unique pointer to extra data which may be empty for most types but for a struct or an array it is going to point to some dynamic data all right and all exist does is it checks to see is this equal to empty if it's not empty then it returns true calculate indexing offset is a very tricky one let's let's come back to that a little bit later has to do with arrays by the way but here's a very important one that allows us to key into a struct pass it a string and navigate into one of the members of that struct has a non Const and a Const version so let's take a look at the implementation of this well the first thing you need to know is the extra data so I got a struct here that holds the extra data just for reasons not that important but then we have struct an array so extra data struct extra data array and they inherit from extra database and the struct 1 the extra data for a struct is it needs a vector of layout elements but it also needs to store the key names for each use elements so it's a vector of a pair pairs of string and layout element I could have also used a map or unordered map but I don't think for the note the number of elements that typically our structures are gonna have I don't think a map or an unordered map would be a good choice probably more overhead than it's worth so we're gonna stick with just a simple vector for right now anyways and that's for struct and a for an array it doesn't need a vector of layout elements it just needs a single layout element that describes its type and that'll be optional because when you create an array remember you first created and then you call set to set its data so initialize data is not going to be initialized so that's why I make this optional and then the size is just the number of elements in the array so these are the optional data boys that those aggregate layout element types will also own now what was I doing here I'm getting sidetracked I believe we were looking into yes the key keying in operator index operator takes a string first we do a simple test to see is this actually a struct this is only a debug time test by the way and release it's not going to do this check for you if so then we static cast our extra data because we don't know what it is remember it's polymorphic pointer to base type we don't know what it is so we're gonna static cast that to extra data struct and we want to iterate through all the layout elements and then we're going to check to see if the key of every layout element is equal to key that was passed in and if so we're gonna return a reference to the layout element which is the second member of that pair if we loop through all the layout elements and we saw them found it we return get empty layout element which let me see if I can find it Oh interesting I just want to scape me I've moved all the definitions over to the CPP file I missed one here but you're here it is get empty layout element it creates a static singleton layout element and that's default constructed so that means that it's type is going to be empty and then we return that but yeah that's how you index into a struct and then the the confusion it just does a Const cast but it calls into this one this is the the reference implementation do you have it that's and that's I mean that's probably one of the more complicated parts of this most of the functions in this class are very simple when taken one by one so yeah that is that is the indexing into a struct this just gets you the type of an array not that complicated get off set begin just returns this offset get off set end is tricky it depends on the type that we have so we end up with here get off sin is a switch switch on the type enumeration value and again we got our X macro so if it's just one of the simple leaf types the offset end is going to be the offset at the beginning plus the size on the GPU pretty simple right but if it's a struct what we do is we get the offset of the end of the last element in the struct and then we add the padding advance it to the next 16 byte boundary so you see the logic is a little different for struct and it's a little different for array because they've got it way you know they have to add padding after the last element stuff like that but that's the basic idea and get size in bytes this one just just offset the end minus offset begin so these guys here because offset is only set after you finalized do not call these guys if you haven't finalized the layout they're not going to give you good day that they're gonna throw an exception in debug mode in release mode they're just going to give you garbage then we got a function to add a new element to a struct it's not that complicated and make sure that the thing we're trying to add to is actually a struct make sure that the name is a valid name then we are gonna loop through all the elements in the name and the struct make sure that we don't we haven't duplicated the name and if not then we're just gonna in place and you lay out our new pair which has the name and has a layout element constructed with that type so I mean there's no real big surprises here add this just sets the type for an array we've got these templates here and this is a little complicated but basically the system went through some evolutions and in previous iteration it used this template here because it wasn't an enumeration it was actually separate types and so we needed to be 10 related and then I rewrote it all after the facts all the code existing code uses this interface I couldn't just get rid of it even though it's not necessary I kept the template version but it just calls the non template version and then here's the resolve this is used whenever we're trying to read or write from bytes in the the raw byte buffer and all it does is it adds a certain here to make sure that the type that we're trying to resolve to is equal to the system type corresponding to the current element so we're trying to resolve to a float and make sure that the thing that we have is actually you know a float and if so it just returns the offset not it's gonna you know throw an exception telling us our programming is bad and dumb and we should redo it also if you try to resolve for something that isn't a base padded data type like a struct or an array that's not doesn't make any sense that's also gonna throw an assertion exception we got constructors they just work basically the way you expect them to work finalized function you call this on the root of the layouts tree and it will recursively finalize all the nodes in the layout tree calculate all their offsets and everything's good so what it does is it works through the tree and you're passing in as it's working to the tree it builds up just the flat array right so your offset is going to be steadily increasing so your finalize keeps track of current offset and when it calls offset or when it calls finalize on a child it passes in the current offset the child returns the new offset after itself and that's how things are built up so I mean if we look at the implementation of finalized if I can find it here yeah very simple for a basic data type we set the offset equal to the offset that we pass in but according to HLSL rules if we've got a float three for example and it's crossing a 16 byte boundary we have to make that float three start at the new 16 at a 16 byte boundary so we gotta push it up add some padding before it so this adds padding before if necessary sets up the offset for the current thing and we got to return the offset at the end of the current element so that's just offset plus hlsl size of the current element so again X macro does that for all the different types making good use of our map but obviously for non-basic types like struct and array there's gonna be more work right struct is going to be recursive and array is gonna be a little bit recursive right so I factored those out into their own functions to make you know make things a little cleaner in here here's the implementation for finalized for struct yeah what do we got here make sure that you actually have at least one element in the struct you advance to the next boundary structure always have to start at a 16 byte boundary and here's where we're keeping track of offsets so we call finalize on each of the members of the struct we get the next offset we pass that offset into the next element and that that is how the the byte buffer is filled basically and of course some of these members here some of these elements could also be structs in which case it'd happen happens similar for finalized for array except there is no loop because an array only has a single type what else we got here advanced a boundary just pushes it up to the next boundary if it's not already on a 16 byte boundary check to see if a block of memory crosses a boundary or not advance if a block crosses boundary validate the symbol name what are the rules for the symbol names I don't remember what I chose the front can't be first no first symbol can't be a digit can't be empty and it all has to be either underscore or alphanumeric there you have it that is your overview of the layout element which forms the the tree that is the map that is the heart of our dynamic constant buffer system the other stuff is snap that hard a layout is just a basic shell that is used to transport this tree of layout elements so it has a pointer to the root of the tree as this and some functions to get the signature of the layout and the size and bytes of the layout that's the the base line which in is inherited from by raw layout allows you to you know index into that that's what that's how you build up your layout as you're defining it and also add very important boy because you're able to add obviously elements directly to the route so you need this interface function here once you once you've gone one level deep you'll have access to all the functions that are in layout where is it layout element right because once you index you get a reference to layout element not another row layout reference I wouldn't make any sense yeah and you know some other functions here clear the thing to deliver the layout the whole tree for you know transplanting somewhere else and you've got a cooked layout which is similar except it allows you to eat mixing but does not allow you to add and when you indexing you get a Const reference so you won't be able to add after indexing either prevents you from mutating the layout which would be a bad thing after it's been cooked would not be a good idea to mutate that but yeah I'm not a big deal here we've got the proxy types the proxy boys here's where all the that syntax magic happens that allows us to you know assign directly to our index things and it just it just looks like something that was built right into the language this is where the sexy happens so as before the allows you to do the exist check well let's take a look at what does an element ref yeah let's take a look at just a normal element ref actually so what what kind of data does it have well it has the offset this is I should have renamed this this the name isn't very good this offset has to do with array indexing which is the most complicated part of this system I believe so I'll talk about last the two main things that these references store is a star point pointer to the layout element that corresponds to the reference and a pointer to the the dumb byte array the beginning of the dumb byte array and so you mean you can index or you can key into them or index into them and that will return another element ref and you know that that corresponds to this operation right here right index into element ref you get another element ref you traverse the tree of elements and check to see if it exists that just calls exist on the layout element incentive exists which is just a wrapper around exists plus assignment the main boys here now the conversion and the assignment these are templated so they will allow you to try to convert or try to assign any type however when you actually do that and the compiler tries to instantiate this template say that's for example you try to assign a float first it's gonna do a static assert it's gonna check to see whether a float is even possible is it in the system if not you're gonna get a compiler error and I demonstrated that in section 1 if it is possible if it is within the type system that's this type that you're trying to convert to then it will perform the resolve on that type and it will do the dynamic type check in debug mode to make sure the types match and it's going to calculate basically this is going to give you the offset into the raw byte buffer and we add that to the beginning of the byte buffer to get a pointer to the bytes of this element and then we're gonna reinterpret those from car into the actual system type forget again forget about this offset for right now I'll talk about that at the end has to do with array indexing but yeah that's for conversion and a similar thing happens for assignment on all assignment does is it calls into conversion so it's static casts to a reference to that type and then assigns so now that I think about it this static assert here isn't strictly necessary it's already covered here but hey might as well give it a good go here as well catch it at the first point of problem but yeah this is the stuff that enables the cool syntax if you want to get a pointer to those bytes we overload the address of operator it returns a pointer which is defined here and pointer actually maintains a pointer to the element ref that pooped it out now this is a little bit dangerous because this can become invalidated if you if you capture this pointer object by value and then the element that spawned it goes out of scope this will be a dangling pointer so this is a this is a disaster waiting to happen I'm aware of it I was aware of it from the beginning and I might fix it in the future I'm entertaining a number of ideas some of them will require a little bit of code duplication but I'm I'm willing to do it some of them I don't know we'll see but anyways about a pointer and a pointer supports and conversion to T star instead of the conversion here to T ampersand and that allows you to do your pointer stuff and there you go there's the thing constant limit ref is the same idea except it doesn't support the assignment and it converts to Const TN and it's pointer class converts to constant T star and by the way non constant element ref has akin implicit conversion ability to constant ref so you can get a constant rep from a non constant one if you want but there you have it those are the boys the proxy boys that do the stuff and finally the buffer and it's not that complicated it just houses a layout route and it has the vector of car which is the raw bytes that actually store the data and again you can index into it with a key two versions non conspiration const version will give you a const element ref that's how you enforce the constant is get the data get a Const car pointer to the data very important for when we actually want to use this for example copy the buffer over to the GPU get the size get the root layout element later on this will be useful for reflection stuff will be able to iterate over the entire structure and read out its dynamic name and type and copy one buffer to another you can get a shared pointer to the layout element notice that it's owning layout element by shared pointer and this is so that multiple buffers can share the same layout layouts are static once you've created them they are immutable right they're cooked and so any number can be shared and that's really good for memory caching yeah there you have it you can create a buffer in a number of ways the most common way is by passing it a raw layout you move a row layout in it will cook the layout and eat the layout delicious but you could also pass it a cooked layout or you could pass it a buffer and it'll copy that buffer and I believe yeah if you copy the buffer it'll copy the buffers layout and it will also copy the bytes of the buffer so that's nice and convenient I didn't implement assignment but you know I could maybe I will someday who knows for right now this is this is doing it for me this is doing it for me good alright now it's time to face the final boss and that is indexing into an array so let's see if I can explain this to you without making your brain explode so let's say we got a structure here's the bytes i won't i won't give each byte its own line i'll give each float its own line so let's say it's got two floats call me f 1 f 2 then it has an array of structures and we'll just make it too wide and those structures each have to float twos so this is padding and here we have this is an array its name is going to be a 1 and it is an array of structure type that structure has two members and those members are going to be named P because my brain is I mean I've been recording for quite a while and my brain is starting to cook in my head so looks like this now normally when you index into a buffer and say you're gonna index assign you want to get f2 what's gonna do is it's going to navigate the the structure well let's draw the structure I guess so you got the base and then you have yeah three members f1 f2 a 1 and a 1 has a type well this is the type T of a1 and it's a structure and it has poo and pee so this is what the tree looks like struct here struct here and array here all right all right so when you index normally into an array and you wanna you know assign to it some value it is going to call resolve and that is going to get the the offset stored in here and that offset it can be directly used to write into the byte buffer so it's as a direct offset number of bytes into the byte buffer but with an array you can't do that if you do buff you know a1 get element 1 which is this one here and then get the P of that element hmm I mean let's think about it for a second this is the this is the conundrum that I had to face when I was implementing this so every element here stores its offset so the offset of the root is beginning which is also the offset of f1 first element of the struct has the same offset as the offset of the struct itself second element it's offset is here the offset of this array is here the offset of this struct is also here the beginning of the array the offset of puh points here the offset of P points here so if we just use the offset of P directly it's always going to access this one but we want to take into account this index number so we have to add an extra array indexing offset so we get the offset from P here and then we add to it the array index and offset which is going to be size of the array the structure key times the actual index number which is gonna be one and that will add from here and get us to here which is the correct answer now the trick here is that you can have arrays that contain structures that contain arrays and those offsets will all add to each other they all cumulate and that is why that is why this exists so when you pop out let me just show you here when you index into the buffer for the first time you're gonna pop out an element ref is gonna have a pointer to the element root it's gonna have a pointer to the key is gonna have a pointer to the element the layout element that corresponds to the key so it's going to index into the element root and get the layout element for that it's gonna have a pointer to the data bytes and the offset is gonna be 0 because there is no array indexing right not yet but whenever you index into an array it is gonna calculate that indexing offset by clicking in the current array index offset and the index value and calculating the new indexing offset and it's gonna pass that in so when you when you calculate indexing offset it's interesting because it actually gets you the tea the whatchamacallit the layout element that is the key of that array and it gets you the new array indexing offset and these build on each other because you pass in the old array indexing offset and it adds to that whereas if you index as a struct it just takes the old indexing offset and passes that to never adds to it so there it is I don't know how many people will actually understand that explanation it's kind of complicated as I've mentioned but that in a nutshell is why you have this size Tina this is an extra offset that accumulates as you index into arrays deeper in the structure and there you have it there is the entire explanation of this system here now there's one final piece of the puzzle here and that is all easier layout codecs so like I mentioned before layouts have signatures and those signatures layouts are once they are cooked and can be registered in this code X and so if you have two different layouts two different buffers that happen to share the same layout they will actually physically share the same layout and they will get benefits of you know memory caching and all sorts of good stuff like that so you can manually cook a layout you can pass in a raw layout to layout code X resolve and it will return to you a cooked layout so what it does is it gets the signature of the raw layout checks to see if that signature already exists in the codex if so it's just going to return the existing cooked one if not it's going to cook that layout deliver route will cook the raw layout and then will poop it out and that will be inserted into the map and we can actually see in the constructor for buffer that takes a row layout it just calls layout codex resolve on that raw layout and there you have it that is my wrong thing here why did I cross the sell it already I wasn't done now I'm done now the implementation overview is complete I have described the the salient parts of the system in a decent amount of detail I don't know you might still be scratching ahead over the array thing I don't I don't blame you it's hard dude it's hard come on the final part here history evolution yeah I want to talk about that now the video is too long already there's no way I'm talking about this however if you're interested I mean the get history is there you can see the old design you can see the new design and how it's much better here you can see heroic rework of dynamic constant buffer system and if you look at it you can see it was basically a complete rewrite however it nice thing because of encapsulation I only had to pretty much rewrite these lines I added a few tests here but I didn't delete anything or change anything but because of a nice encapsulation again as we often see in this series don't have to go and change a bunch of other code that depends on this I just have to change the internal implementation and all the code that depends still keep working like it would work before but yeah you can look at this you can see the code before this commit and after this commit and you can see it's very different if you're interested in you know talking about this stuff the history and evolution of the system you can always you know hit me up on the discord or on the forum and I'll be happy to talk shop with you I don't think we need to bloat this video with any more discussion like that that's not gonna be necessary but anyways there you have it and I don't know what do you guys think I'm really happy with the system was working out great so far believe me I'm aware it has its limitations it has its things like well one big one is that the issue I showed you with pointer and I will you know correct them as I seem necessary but for right now it's it's working really good for my purposes and I hope you guys get a kick out of this too cuz it really shows you what you can do with Si plus it's that you can remould almost the core language if you want to add like new features to it that's the meta programming power and you know you don't want to spend all your time in that design space you'll never get anything really really accomplished but a judicious amount of time spent there can really benefit the entire experience and the success of a large enough project and that's the kind of thing that does C++ enables that not many other languages enable so I hope you guys got a kick out of this and you know if it's not your it's not your cup of tea don't worry next video is gonna be less of this kind of stuff and it's gonna be more flashy graphics kind of stuff specifically we're gonna be doing a little bit of a special effect using the stencil buffer so I'll be teaching how to use the stencil buffer and we used to do a little special outlining effect and that's gonna lead into some other stuff about full-screen processing it's gonna be a good time when chillin thanks for watching hope you enjoyed this video if you did please click the like button helps a lot and now I will see you soon with some more hardware 3d [Music] [Applause] [Applause] [Music]
Info
Channel: ChiliTomatoNoodle
Views: 4,019
Rating: undefined out of 5
Keywords: 3d game programming, c++ 3d, game programming tutorial, c++, C++ tutorial, c++ game engine, how to, 3d game engine, Directx programming, direct3d programming, how to program 3d games, DirectX, Direct3D, D3D, programming, game, lesson, cpp, guide, code, tutorial, coding, software, development, reflection, dynamic data, constant buffer
Id: y7rhMx2pD9k
Channel Id: undefined
Length: 78min 3sec (4683 seconds)
Published: Fri Dec 13 2019
Related Videos
Note
Please note that this website is currently a work in progress! Lots of interesting data and statistics to come.