Graphics vs Compute Pipeline

The Cherno · Beginner ·⚡ Algorithms & Data Structures ·1y ago

Key Takeaways

The video compares the graphics pipeline and compute pipeline for image rendering using OpenGL and GitHub, with a focus on retrieval augmented generation and RAG search, and demonstrates how to optimize the compute pipeline for better performance.

Full Transcript

So, recently I made a video that showed you how to take something from 30 frames per second to 7 1/2,000 frames pers by using a little something called the GPU instead of the CPU for some image rendering work. The results were crazy and it wasn't even an optimized solution. We could have definitely refined the compute shader and made it not write twice as many pixels, for example. But also, compute shader. I chose to implement this inside a comput shader using this little project I threw together called GPU compute, which was just a super simple example that used OpenGL to let you run arbitrary compute shaders. That made the most sense to me because we didn't really need like the full graphics pipeline because we weren't doing any 3D rendering. We just wanted to kind of render an image that filled our window and that was it. So, in my eyes, that seemed like the most simple solution. However, I thought it would be fun to try and implement basically the same solution but using the graphics pipeline, using a pixel shader because that would let us first of all compare the two different techniques, the graphics pipeline versus the compute pipeline for this specific task. See what the performance is like between the two and what the actual implementation experience is like as well. But then also some people were curious in the comments of the last video of why I didn't go with like the fragment pixel shader approach versus the compute shader approach. And so here we are. We're going to see what it's like to use the graphics pipeline for this task. My prediction is that it's going to be extremely close. I mean, we're already at like 7 half thousand frames per second for the compute shader. It shouldn't be faster theoretically. Like, I'm pretty sure it's basically impossible, unless I did something wrong, for it to be faster than the compute shader approach. The reason why is because the compute shader, the compute pipeline, that way of doing things is just a much more stripped down raw version of what the graphics pipeline is doing. In this case, the graphics pipeline is just going to involve so much extra stuff. Like for example, we'd have to invoke the vertex shader at least three times to draw like a full screen triangle if we wanted to. If you actually have a look at what the graphics pipeline looks like, you can see there are just so many different stages. And for something like this, I just don't think it makes sense. So, I'm pretty sure that it will be slower than the compute pipeline that we implemented last time. However, I do think it's going to be very close because it's just this is just easy for the GPU to do in general. Speaking of easy, Bootdev, the sponsor of this video, makes learning programming super fun and much easier. Boot.dev is a website that takes inspiration from modern game design to teach programming in a much more fun, engaging, and practical way. They've got lots of hands-on courses that balance theory really well with practice. As you make your way through the lessons, you'll be writing code as you go, reading existing code, fixing bugs, you know, just like a real programmer. And that's what makes it so effective. And they tie this in with gamelike systems really well. So, you'll be earning XP, leveling up, paying your mentor with resources that you earn, salmon, which makes learning so much more fun and communicates a sense of progress really well. Speaking of your bear mentor, it's an AI chatbot that's specifically trained on the course data. So, if you need help or you have additional questions that go a little deeper, you can just talk to it. This is something that I personally missed from university back in the day because I had all these additional questions. I wanted more information. I wanted to go deeper, but the teachers just couldn't help me out. And check this out. All of their content is completely free to read and watch in guest mode. You'll need a paid membership to access the interactive features such as the hands-on programming. There's lots more to discover. Head to boot.dev. Link will be in description below and check it out for yourselves. You can also use code churn to get 25% off your entire first year of membership. Huge thank you to Bootdev for sponsoring this video. Just going to take this off cuz we're about to get down to business. Toasty. Okay, so first and foremost, I have put together this GPU compute repository. I did this for the last video. So link will be in description below. It's just public on GitHub. I'm going to update it for this video actually. Basically the only difference is I've added these two create graphics shader and reload graphics shader functions into the shader class. The reason why is because before we could only create compute shader specifically you can see over here. Whereas this code just allows us to pass in two paths here. A vertex shader path and a fragment shader path. And it will just compile both of the shaders link them into a program. So we have like a normal graphics pipeline shader program and then the reload graphics shader function will just try and reload the vertex shader and fragment shader so that we can kind of recompile the code and iterate much faster. That's like the only change. Other than that, back in main, we're going to continue on from where we left off last time. Here it is. This is the compute shader version running pretty quickly as you can see over here. So what exactly do we need to transition this from the compute pipeline to the graphics pipeline? Well, as you're about to learn, we need a lot more stuff. We're going to need something called a vertex array and a vertex buffer at the bare minimum. We are using OpenGL, which is why vertex arrays are a thing. We're also going to need a vertex buffer because that's going to hold like the vertices that are going to make up the geometry that we now have to render. And then finally, an index buffer isn't probably strictly required. I'm probably going to start off by just drawing a quad. I could draw this just by drawing two triangles like this, which would be okay. that would require six indices because we would basically be drawing 1 2 3 and then 4 5 6 not necessarily in that order but we'd be drawing six vertices to make this up. However, we can actually get away with using just four indices by drawing something called a triangle strip instead of a triangle which is just a way for us to basically reuse these two vertices without specifying that we are using it. So, a triangle strip is really good for if you have like, I don't know, let's just say a blade of grass, cuz that's the the first example that comes to mind. If you have a blade of grass, because you'd be drawing basically like this, it's just a way for us to be like, okay, we're going to continue on like from here and grow that way instead of having to redefine, resppecify that we want to render those vertices using like the indices every single time. However, let me just say that if you are drawing a triangle strip with four indices, that's going to technically invoke the vertex shader four times, once per vertex. And of course, a quad has four vertices. You need at least four vertices to define a quad. That is the definition of a quad. So therefore, you have to position these four vertices somewhere inside your screen. And this kind of makes up that full screen quad. But it's actually possible for us to instead draw a full screen triangle which this is a little bit of a weird concept I think if you're completely new to this but basically if you picture our window this is the window we are rendering to like this. Yes, we could draw a quad inside it like this as we currently were discussing. However, we could also draw a triangle that looks something like this because even though this stuff is kind of seems like a waste, it's not really because it's never going to get filled with pixels, we're only going to be running our pixels over this portion of our screen because everything else gets clipped. And obviously, a triangle has three vertices instead of four. So typically the more efficient approach would be in fact to do a full screen triangle like this versus a full screen quad like this because of the one extra vertex shader invocation essentially. It's pretty easy to show both approaches. So I'll probably just do that just to hopefully teach you guys a little bit more. So enough explanation. Let's actually start programming now. So I'll make a little bull here called triangle because I want to demonstrate both the triangle and the quad version. So we'll start by doing not the triangle version which is the quad version. The common code between them is going to be the fact that we're going to have to call GL create vertex arrays because we're going to have to create a vertx array no matter what. This is going to be a G uint and we're going to call it vertex array. Over here we just say we want to create one vertex array and that's what this code will do. It will just create a single vertex array. Give us the ID. Vertex array is obviously empty. Doesn't have anything in it but it does exist. And we'll also do the same thing for create buffers because we need a vertex buffer. Now, an index buffer is going to be necessary, but only for the non-triangle approach. I obviously could have created an enum for this. Probably would have been a little bit more clear than just trying triangle equals false, but whatever. You'll just have to make sure you're paying attention. Okay, so we have an index buffer, we have a vertex buffer, we have a vertex array. Let's actually maybe create the vertices. Now, aside from the actual positions, which if we have a look at our window that we're rendering to, if we don't use any kind of projection matrices, which we will not be using, the space that we're in, these like normalized device coordinates are -1 to one like this. So, we need to make sure that we have a negative 1 vertex position as our minimum and a positive one vertex position as our maximum. So, this is pretty easy. This will just be one negative 1 for like this bottom left corner. Then we'll go counterclockwise because that's the default winding order. And we basically end up with something like this where this one here, the negative 1 one is over here. Then we have positive 1gative 1, which is this one. Then we have 1 one, which is here. So I actually need to flip that. And then we have negative 1 on x and positive one on y, which is that last one. However, we're also going to need UV coordinates since we're going to need to know where exactly in the rasterized image we are. Remember what we're drawing here is basically this quad that fills our screen. But since the rendering algorithm needs to know exactly where we are within the quad to determine what kind of color to shade that pixel, we need to know our coordinate within that image. And that's where a UV coordinate comes in. So those texture coordinates are between 0 and 1 in both the X and the Y axis. So for the minimums, they'll basically be zero. And then for our maximums, they'll be one. So, basically, wherever you see a negative, you should see a zero. And wherever you see a positive, you should see a one. So, that's all I'm going to do. I'm just going to go through and make that happen. Okay. Well, this is wrong because we don't we don't have positive 1, positive 1 anyway. So, where did I go wrong? That should be this one. So, this should be positive 1. Positive one everywhere. And then, how did we mess this up so badly? Negative 1 1. This should also be negative 1. Okay, there we go. And this should be positive one. Now, another thing that I totally forgot about is because we're doing a triangle strip, we kind of have to go like this, which means that instead of going just counterclockwise around the quad like we usually would if we were drawing six indices, we'll actually have to go from this bottom right to the top left. So, since this is the top left, we have to rearrange this slightly. And then, of course, the 1 one also has a 1 one texture coordinate. And then, for clarity, I've added these here. The indices are going to be super simple for this. like literally 0123 and that's it. Little bit of a spoiler, but we're not actually going to need an index buffer, but whatever. I'll show you how to do it with an index buffer. So, GL named buffer data is going to allow us to upload this data now to the GPU. By the way, if you did watch that last video, you can see just how insane this is versus just using a compute shader. So, maybe now it kind of makes sense why I decided to go with a compute shader. So, we have our vertex buffer that we want to upload data to. We're just going to do size of vertices and vertices with the usage of glatic draw because they're not going to be changing. Same thing for the index buffer except obviously with the indices. GLvertex array vertex buffer is just going to allow us to connect this vertex buffer to the vertex array at a binding index of zero. So vertex buffer offset zero and then the stride is going to be these like four floats. So the stride is just the amount of bytes between vertices. So let's just do size of float times four. Now we have to what is it? Vertex array trib enable g enable vertex tri array trip. This man openg takes you back. So this is we're going to have to do this for 0 and one because we're going to be using the position as attribute zero and then the texture coordinate will be in attribute one. So we just have to enable those two. Then we have to specify the format. So this just explains what exactly we have in this attribute. And we need to basically define these attributes because we want to be able to access this data inside our vertex shader. So for attribed, we don't want OpenGL to normalize it. So GL false. And then relative offset will just be zero. And then for the next one, it's going to be exactly the same, but the offset is going to be the size of that previous one. And obviously, we are in index one. GLvertex array vertex a trip binding. Oh, it's just a trip binding because this is the like direct state access and we haven't actually bound our vertex array. We basically have to specify that we want this vertex buffer here to take on this layout. So for our vertex array, attrib. So remember, we bound this vertex buffer into binding index zero. The reason this is important is because you can have many vertex buffers inside a vertex array. You might have an additional one for bones or for transforms in your scene. So it's important that we specify that we want these attributes to apply to specifically this vertex buffer that we bound over here. And then finally the uh element array buffer which vertex array I guess element buffer. Yeah, we need to just specify that we want vertex array to be bound to this index buffer. I refuse to call it an element buffer. It's called an index buffer everywhere else. And that I think is it. So we should just be able to do a draw call now. So we have this compute thing here. I guess I should make a little if s compute. We'll make a compute state cuz I do definitely want to be able to toggle between the two. So up here where we have our statics defined, we'll say bull s compute equals let's do false cuz we want to run it through the graphics pipeline. And then we'll do if s compute and then we'll do else. So this is going to be the graphics pipeline version. And for that we need to make sure that we bind oh we're going to need a render target as well. So bind frame buffer. We should just be able to render to this frame buffer that we already created with this compute shader texture. So, if we just bind, I think gel frame buffer FB handle, we should be able to render to it. But then, because we're blitting that frame buffer to our spot chain, we can unbind it when we're done with it. And everything should still work because the texture within that frame buffer is then going to get blit onto our swap chain. So, that actually should work out really well. So we'll bind the vertex array and then we can just do gl draw elements since we are using that element buffer. Well I did call it an element buffer and we'll do gl triangle strip count. So for type gl this is the type of data inside the index buffer. And then for indices null pointer because there's no offset. So that excluding the shader is all the extra work that we need to do. All of this right is all of the extra work that we need to do versus just running the compute shader. So it is a lot. Now speaking of the shader, let's actually write a really simple vertex and fragment shader. So if we go into the shaders directory, I've already written a very basic vertex and fragment shader just to speed things up. This is the vertex shader. In fact, let's also test out our text cores. That might be good. We're going to have to send them out. And in fact, let's make both of these vect twos. Add a zero here. I've already changed my mind compared to when I first wrote this like a few minutes ago. We'll send out this text cord into our fragment shader. We'll make sure our fragment shader receives it. And then we can try and output it as a color. But let's just try pink first. And of course, back in our vertex shader, we'll just make sure that the texture coordinate is in uh location one because that's what we specified for our attribute over here. Location one is our texture coordinate. So now let's load this shader. So that should be pretty easy. The functions inside shader just return the shader handle. So that's why we have s compute shader here. So let's call this s graphics shader. And then over here where we load the compute shader, I'll duplicate this. We'll do s graphics shader just to make sure that we don't run the program if that doesn't work. And then we'll just change this to create graphics shader. And then for the path, let's duplicate this vertex shader path fragment shader path vertex and fragment. And then over here into create graphics shader, we will pass in vertex shader and fragment shader path. Now I'm going to do one better here and over here inside our key call back where we reload this with the R key. Let's reload both. So we'll also do S graphics shader reload graphics shader and then we'll pass in the graphics shader handle the vertex path and the fragment path so that we can also reload that shader. And then finally we need to remember to bind it. So GL use program s graphics shader. And that should be it. And in fact, let's also since we have this s compute state up here, let's go between states. So we'll say if key equals I guess S for state, we're just going to do S compute equals not S compute. So it will flip between that. Now we will get repeat key presses. So the best thing we can probably do is just to say that if the action isn't GLFW press, then we'll just return. So, we'll only react to the first time the key is pressed, not for repeat key presses, which is when you hold down the key and it triggers multiple times. Otherwise, we'll keep flipping between these states. And you know what? One more thing I think I'll do is the title says compute. So, let's change this. This will only trigger once per second, so it'll might be a bit delayed, but let's change this to reflect what state we're in. So, if we're in compute, we'll say compute. Otherwise, we'll say graphics. Okay, that looks pretty good. Let's go ahead and run that. I think we start in the graphics state. So hopefully we'll see pink. And then you can see it says graphics up here. Then if I hit S, we go back to compute. Takes a second to change the title. And we're in compute now. And now we're in graphics. So there you go. You can go between the two by pressing the S key. And if I put this side by side and I go back to our fragment shader and I say that I want to instead look at the vtx chord and I reload this, we get blue, which is not what I was hoping for. Ah, this says out vtx chord. Okay, in vtx chord. There we go. Okay, so that is actually wrong. That looks like it's it's between negative one and one. So, this should actually be over here. I wonder if I got the offset wrong. Oh, you know what it is? This uh this is the worst thing about OpenGL. So, this says size, which kind of implies to me that it would be in bytes, but it's not because if you look at the documentation, the size is the number of values per vertex that are stored in the array. So, it should just be two, not size of two. I know, ridiculous. And of course, the other thing that I butchered was this was supposed to be a zero. Remember, I said this myself and yet it's getting a bit late. I'm a bit tired. If it's negative, it's zero. If it's positive, it's not zero. So, forgive me. That is the correct thing hopefully. And now, there we go. That's what I'm looking for. That's why it's really important to visually debug things because obviously if I had had no idea how to do that, I'd just continue writing my shade and be like, why is nothing rendering correctly? You got to get this right. Now, what's funny is that like we are basically at the same frame rate. I would say like almost a little bit slower. Like if I hit S to switch back to compute, you can see the FPS actually rises a bit. So not very promising for those of you who are suggesting the graphics pipeline, but nevertheless, we will keep going. In fact, let's already make an optimization to this and not use the quad approach, but instead use the triangle approach. So let's grab we don't need the index buffer. Let's grab the vertices. Because the triangle is going to be quite a bit bigger than our actual screen, it basically means that we have to use some special coordinates. So instead of one, we'll actually use two. And the range of our triangle is actually from -1 to three in terms of like the position. So it should look something like this. And then the rest is the same. We're just going to copy everything else that we have here. Obviously, the index buffer is not something we're going to use at all, but everything else stays the same. And so because of that, I'm actually going to take out this from that branch, leave it over here, and hopefully that will just work. We'll just need to set these actual index buffer and vertex buffer data so that we actually upload it to the GPU. And of course the element buffer has to apply to this branch but not to this branch. I think that should maybe work. So let's try it again just with the quad. And then now let's set triangle to true and we'll see if we get the same result. Okay, we get a crash. Nice. Oh yeah. Well, obviously because our draw call needs to be changed as well. So the draw elements. So if triangle, we just do a glraw arrays. gel triangles zero and three. Otherwise, we'll do a draw elements like this. So, let's try this. And there we go. Exact same result with this branch over here being hit. So, we now have our full screen triangle. Let's look at the performance. Yeah, I mean about the same. Actually, it does seem a little bit. Yeah, there we go. We were not getting to 7,000 FPS before. We're not really getting below 7,000 that much. Whereas, if triangle is set to false so that we do the full screen quad, you can see that performance is a lot of it is below 7,000. and we don't seem to get up to 7 1/2 at all. So yes, definitely seems to be a little bit faster, but of course we are in that range where you know we're already really fast. But I think it is worth noting that the one less vertex like that, you know, is helpful. Okay, so now we are finally up to the juicy part of this video, which is how do we take our existing compute shader, which is over here. So how do we go from this, which is what we currently have, into this fragment shader? Well, the good news is it's actually pretty easy because most of what this does, the first half actually sets up a state that we already basically have over here. And that is this pixel coordinate being turned into this normalized coordinate which then does get flipped. But basically, this is a coordinate between 0 and 1, which is our texture coordinate. Now, F text size is also necessary, which we'll take a look at. But otherwise, let's just grab maybe from here onwards. We don't care about calculating the pixel coordinate because that's what this is. And we also have gel frag cord if we need it. The texture size is something we don't have. We could pass that in as a uniform, but for now, we can just say 1280 x 720. So from here onwards, let's go ahead and copy this code into our fragment shader. I'm just going to hardcode the text size to be 1280 x 720 because that's what we know it is initially when we start our program before we resize. This normalized coordinate is just going to be vx chord. Now pixel coordinate is used over here. However, I think we could probably just use our normalized coordinate instead. It's just that if we do, we obviously can't divide it by the text size divided by two. So this is dividing it by half the text size which is basically going to put it into a spot that I think is going to be from -1 to 1. So I believe we should be able to do this by just doing normalized cord.y * 2 -1. Then we can do normalized cord doy greater than zero. Same situation over here. F sample width is just pixel cord divided by the texture size which is already that normalized coordinate. So we can probably optimize the computer tra a little bit by not needlessly doing these calculations again. Now sky sample ground sample look fine. These are just sampling textures and grabbing the color that we need. The image store is obviously going to be different. But also this is the problem with the computer shader. We were doing this twice. So for every pixel on the image we were actually writing two pixels which is redundant. We should have been doing one of these. So for our fragment shader instead of image store we have an output color right this O color over here. So we need to make sure that that's what we output. So let's go ahead and set the output color. Yeah, sure. Let's just do vec ground sample. We'll just set it to the ground sample as a test. And I'll keep this around cuz I just want to get this compiling first and foremost. And then I think we also we definitely also need to take in all of this stuff. So near fast sky texture ground texture. I'll keep them at one and two because we have zero here taken for an output image which we don't need here. But if I keep them at binding one and two, it means that I don't have to bind them at different binding points for the compute versus the graphics pipeline version. So in other words, this bind texture unit and these this jelly uniform, these just stay exactly the same. So once we use program, we can bind the two. Now near and far, all of this stuff that we're calculating up here is obviously not specific to the compute branch anymore. So we'll move it up here. And then finally, we could move this GLU's program out. And that way we could keep these in a common place as well. But I don't really mind duplicating them to be honest for this example. So we'll leave it as is. That I think is it. Should we Let's just run this. See like what happens. Obviously we're just testing so we won't see the right result. But we see something pretty close. The S key will flick between compute and graphics. But we already get something that's really close. So now the original code was basically doing this weird thing. This is Yeah, I don't really like this. So the bottom part is our ground and the top part is our screen. And so what it was doing was basically having the texture size. So getting the midpoint and remember with compute shaders we're actually outputting to the pixel coordinate. So not between like 0 and 1 or anything like that. We actually had to specify the pixel coordinate. So like something like you know 540 by 300 would be an example of that. So the space is definitely different. But the point of this was basically to just have the ground be like for this section and then the sky be for this section. So we were adding on the pixel coordinates starting from here onwards to this part and then from here onwards for the ground inside our fragment shader. What we can do instead is probably do a similar thing to this because we know that it's going to be between 0 and 1. So if we just said something like hm let's see. So like if it's above 0.5 which is exactly half of the screen then we must be the sky versus the ground and that's it. And yes this is a little branch but branching in GPU shaders like I know a lot of people think that it's really bad for performance but it's not. This is fine. Trust me this is fine. So now if we run this. Okay we're upside down. Great. Actually not only are we upside down but if you have a look at it these are scrolling in opposite directions. Now I think that is due to the way that the sample depth is working because at the end of the day that specifies where to actually sample from. So one way we could debug this is if we just compare f sample depth. So let's output vec for f sample depth and one. I don't think it actually matters with the whole alpha thing cuz we're not actually doing any blending. But we'll output f sample depth and have a look at it. So you can see this is just a gradient whereas I think what I'm expecting to see this is a bit of a guess but I kind of know what I'm doing is this is going to be a gradient from black which is our kind of vanishing point all the way up to white. Again we can confirm this if we look at the compute shader and we output that fs sample depth as well. So let's do image store output image just pixel cord and then we'll do this. We'll obviously have to make this an actual color. So let's do something like that. try run this and then if we switch to compute. Okay, so this is one little thing about compute shaders, but we should be able to do that. Oh, okay. So, it's actually flipped. But the problem is that it's not just that. It's also the fact that this is bizarre. Like this is different to our approach. So, let's go back to this normal state for our compute shader. And then what I'm going to do is I'm going to make this absolute. So, that's going to give us that gradient that we're looking for like this. And hopefully, I mean, I'm pretty sure that should just work because of our new sampling strategy. Okay, beautiful. It does. Now, we are upside down. I think the reason we're upside down is because we're doing this normalized core.y, right? We're inverting it. So, we're doing one minus that. So, if we just get rid of this line of code and reload our shader. There we go. This is graphics. This is compute. Can you spot the difference? I can't. So, let's maybe do this. the sky. I'm going to add like a little bit of red to the sky just so that we can flick between the two. And now you can actually tell which one you're in just to make sure that it's actually working. Now, with these changes though that we've made, we're obviously only outputting a single color per pixel. So, we're outputting a single pixel per pixel. Let's make our compute shader do that as well. So, what I want to do is this whole normalized cause situation that we worked out, let's also do this in the compute shader. So I'll replace that code with this. And then for these two, we can just use a branch. Now branches in compute shaders are a little bit different, but I don't want to over complicate this. So we're not going to get too deep into this. But let's do this normalize core.y greater than.5. We'll also probably I imagine have to get rid of this line, which is good. Saving an instruction probably. We'll do the sky up here cuz that's the greater branch. And then we'll do the ground down here. Let's also do the sky in this branch cuz that makes more sense and the ground in this branch. And then finally, we don't need this weird coordinate situation anymore because I think we we should just be able to use pixel coordinate because now we're actually in the appropriate context and we've actually rewritten this to go pixel by pixel instead of trying to do like this weird two rendering mirrored kind of flippy situation which was also weird. That's why the whole inversion thing, I guess, also happened because we were It's like we were doing a mirrored version since we had the minus and the plus, so they were actually going in opposite directions when we were drawing the the pixels at that coordinate. Anyway, this um should hopefully be better. So, let's have a look. This graphics, this compute. Okay, so exactly the same. And in fact, this is a bit faster now as well at 8,000 frames per second. Cool. So, cleaning up the shaders a little bit. The one flaw I guess we have here is that the texture size is not actually like it's not resizable. If we try and resize this, we're not going to render into the full screen whereas we will with the compute shader. So that's kind of a nice detail. But maybe that can be something you guys can uh implement for homework as an exercise if you like. Just take it in as a uniform. You could also actually take in the texture and then use that same image size function. The choice is yours. So now here is the final performance that we're seeing here. 12,179 framesps. 12,000 frames pers in the compute version now that we're not drawing multiple pixels. And if I switch to the graphics version, then we are a little bit slower here at 11,000 frames pers. So compute 12,000, graphics 11,000. both extremely fast, but we have that additional lack of work that needs to be done if we use the compute pipeline instead. Now, the last thing that I thought would be kind of fun is to take a look at render doc. And you can see, by the way, so we've already rendered 125,000 frames since me starting. Look how quick this frame counter is going up. It's insane. But anyway, this is the compute shader. This is the graphics pipeline. uh if we just capture a frame here. I just thought it would be nice to actually see I mean let's also capture a compute frame but ultimately I thought it would be nice to see so this is the graphics frame over here for those of you who might not completely understand what that triangle the full screen triangle is doing. If we have a look at draw arrays you can see we are drawing a triangle and our vertex shader output looks like this. Now this may look a bit weird. I was hoping it would be a bit more clear. If we don't highlight vertices, you can actually see over here in white, it's showing our window, our frame buffer, like that we're actually rendering to. So, it's this like white outline that goes like this. Actually, I think I can draw, yeah, I can draw rectangles here. So, that's what it looks like. And then you can see the triangle goes off here. So, it's a full screen triangle like this, but we're basically just going into this area over here. That is what that looks like. If we have a look at the compute frame, you can see it instead does a GL dispatch compute. And this is our pipeline, just the compute shader. I'll try and make this a little bit more visible. So, we just have the compute shader. None of this pipeline is being invoked, right? It's just the compute shader and that's it. Whereas over here with the graphics pipeline, check this out. So, we got vertx input, vertx shader, these aren't being used. rasterizer, fragment shader, frame buffer, like all of these stages and all these different players here are at play versus just the compute shader. And that is why ultimately we see such a performance difference. Again, at this level, the frame rate is going to fluctuate wildly. But like for example, running it right now, you can see I'm getting about 6 1/2 to 7,000 FPS on the graphics pipeline. But if I hit S and I switch to compute, you can see I get quite a good boost. So there's compute hitting 8,000 sometimes. And there's graphics not even really being able to get up to 7,000. If you want to play around with this, I will leave a link to the repository below that contains all of the code for both this graphics and compute pipeline version. Let me know what you guys thought of this kind of video. Honestly, I love making videos like this, just kind of exploring different things. I think it's really helpful and important to actually do stuff hands-on. This is computer science. We shouldn't just be theorizing. Doing stuff in practice, getting real experience like this is super important. Anyway, I hope you guys enjoy this video. Don't forget to check out Bootdev using my link in description below and I will see you next time. Goodbye.

Original Description

Click this link https://sponsr.is/bootdev_cherno and use my code CHERNO to get 25% off your first payment for boot.dev. Patreon ► https://patreon.com/thecherno Instagram ► https://instagram.com/thecherno Twitter ► https://twitter.com/thecherno Discord ► https://discord.gg/thecherno 🔗 LINKS GPUCompute ► https://github.com/TheCherno/GPUCompute Code from this video ► https://github.com/TheCherno/GPUCompute/tree/graphics Hazel ► https://hazelengine.com 🕹️ Play our latest game FREE (made in Hazel!) ► https://studiocherno.itch.io/dichotomy 🌏 Need web hosting? ► https://hostinger.com/cherno 📚 CHAPTERS 0:00 - Intro 3:30 - Rendering a fullscreen quad/triangle 21:21 - Compute to fragment shader porting 28:25 - Optimizing our compute shader 30:24 - Final results 💰 Links to stuff I use: ⌨ Keyboard ► https://geni.us/T2J7 🐭 Mouse ► https://geni.us/BuY7 💻 Monitors ► https://geni.us/wZFSwSK This video is sponsored by Boot.Dev.
Watch on YouTube ↗ (saves to browser)
Sign in to unlock AI tutor explanation · ⚡30

Playlist

Uploads from The Cherno · The Cherno · 0 of 60

← Previous Next →
1 3D Game Programming - Episode 1 - Window
3D Game Programming - Episode 1 - Window
The Cherno
2 3D Game Programming - Episode 2 - Game Loop
3D Game Programming - Episode 2 - Game Loop
The Cherno
3 3D Game Programming - Episode 3 - Arrays
3D Game Programming - Episode 3 - Arrays
The Cherno
4 3D Game Programming - Episode 4 - Drawing Pixels!
3D Game Programming - Episode 4 - Drawing Pixels!
The Cherno
5 3D Game Programming - Episode 4.5 - How Rendering Works
3D Game Programming - Episode 4.5 - How Rendering Works
The Cherno
6 3D Game Programming - Episode 5 - Playing with Pixels!
3D Game Programming - Episode 5 - Playing with Pixels!
The Cherno
7 3D Game Programming - Episode 6 - Performance Boosting
3D Game Programming - Episode 6 - Performance Boosting
The Cherno
8 3D Game Programming - Episode 7 - FPS Counter
3D Game Programming - Episode 7 - FPS Counter
The Cherno
9 3D Game Programming - Episode 8 - Alpha Support and More
3D Game Programming - Episode 8 - Alpha Support and More
The Cherno
10 3D Game Programming - Episode 9 - Beginning 3D
3D Game Programming - Episode 9 - Beginning 3D
The Cherno
11 3D Game Programming - Episode 10 - Floors and Animation
3D Game Programming - Episode 10 - Floors and Animation
The Cherno
12 3D Game Programming - Episode 11 - Rotation
3D Game Programming - Episode 11 - Rotation
The Cherno
13 3D Game Programming - Episode 12 - User Input
3D Game Programming - Episode 12 - User Input
The Cherno
14 3D Game Programming - Episode 13 - Render Distance Limiter!
3D Game Programming - Episode 13 - Render Distance Limiter!
The Cherno
15 3D Game Programming - Episode 14 - Basic Mouse Movement
3D Game Programming - Episode 14 - Basic Mouse Movement
The Cherno
16 3D Game Programming - Episode 15 - Textures + More!
3D Game Programming - Episode 15 - Textures + More!
The Cherno
17 3D Game Programming - Episode 16 - Walking, Crouching, Sprinting + More
3D Game Programming - Episode 16 - Walking, Crouching, Sprinting + More
The Cherno
18 3D Game Programming - Episode 16.5 - Exporting Runnable Jars
3D Game Programming - Episode 16.5 - Exporting Runnable Jars
The Cherno
19 3D Game Programming - Episode 17 - Small Adjustments + Birthday!
3D Game Programming - Episode 17 - Small Adjustments + Birthday!
The Cherno
20 3D Game Programming - Episode 17.5 - Creating an Applet
3D Game Programming - Episode 17.5 - Creating an Applet
The Cherno
21 3D Game Programming - Episode 18 - The Beginning of Walls
3D Game Programming - Episode 18 - The Beginning of Walls
The Cherno
22 3D Game Programming - Episode 18.1 - A Few More Things
3D Game Programming - Episode 18.1 - A Few More Things
The Cherno
23 Episode 18.5 - Creating an EXE File in Java
Episode 18.5 - Creating an EXE File in Java
The Cherno
24 3D Game Programming - Episode 19 - Rendering Walls
3D Game Programming - Episode 19 - Rendering Walls
The Cherno
25 3D Game Programming - Episode 20 - Continuing Walls, Fixing Bugs, and Managing Crashes
3D Game Programming - Episode 20 - Continuing Walls, Fixing Bugs, and Managing Crashes
The Cherno
26 3D Game Programming - Episode 21 - Texturing Walls, Fixing Clipping, and Fixing the Mouse
3D Game Programming - Episode 21 - Texturing Walls, Fixing Clipping, and Fixing the Mouse
The Cherno
27 3D Game Programming - Episode 22 - Random Level Generator + Properly Fixing Clipping
3D Game Programming - Episode 22 - Random Level Generator + Properly Fixing Clipping
The Cherno
28 3D Game Programming - Episode 23 - Graphical User Interface (GUI) Launcher
3D Game Programming - Episode 23 - Graphical User Interface (GUI) Launcher
The Cherno
29 3D Game Programming - Episode 24 - Making Our Launcher Work
3D Game Programming - Episode 24 - Making Our Launcher Work
The Cherno
30 3D Game Programming - Episode 25 - Writing and Reading Files
3D Game Programming - Episode 25 - Writing and Reading Files
The Cherno
31 3D Game Programming - Episode 26 - Custom Resolutions
3D Game Programming - Episode 26 - Custom Resolutions
The Cherno
32 3D Game Programming - Episode 27 - Decorating the Launcher
3D Game Programming - Episode 27 - Decorating the Launcher
The Cherno
33 3D Game Programming - Episode 28 - Continuing our Custom Launcher!
3D Game Programming - Episode 28 - Continuing our Custom Launcher!
The Cherno
34 3D Game Programming - Episode 29 - Launching The Game
3D Game Programming - Episode 29 - Launching The Game
The Cherno
35 3D Game Programming - Episode 30 - Colour Processing In-Depth
3D Game Programming - Episode 30 - Colour Processing In-Depth
The Cherno
36 3D Game Programming - Episode 31 - Sprites!
3D Game Programming - Episode 31 - Sprites!
The Cherno
37 3D Game Programming - Episode 32 - Sprite Mapping
3D Game Programming - Episode 32 - Sprite Mapping
The Cherno
38 3D Game Programming - Episode 33 - High Resolution Rendering
3D Game Programming - Episode 33 - High Resolution Rendering
The Cherno
39 3D Game Programming - Episode 34 - Entities
3D Game Programming - Episode 34 - Entities
The Cherno
40 Genesis - My Game for Ludum Dare 24
Genesis - My Game for Ludum Dare 24
The Cherno
41 Vlog + Ludum Dare Results
Vlog + Ludum Dare Results
The Cherno
42 Game Programming - Episode 1 - Resolution
Game Programming - Episode 1 - Resolution
The Cherno
43 Game Programming - Episode 2 - Threads
Game Programming - Episode 2 - Threads
The Cherno
44 Game Programming - Episode 3 - Game Loop
Game Programming - Episode 3 - Game Loop
The Cherno
45 Game Programming - Episode 4 - Window
Game Programming - Episode 4 - Window
The Cherno
46 Episode 5 - Buffer Strategy
Episode 5 - Buffer Strategy
The Cherno
47 Game Programming - Episode 6 - Graphics Initialized
Game Programming - Episode 6 - Graphics Initialized
The Cherno
48 Game Programming - Episode 7 - Buffered Image and Rasters
Game Programming - Episode 7 - Buffered Image and Rasters
The Cherno
49 Game Programming - Episode 8 - The Screen Class
Game Programming - Episode 8 - The Screen Class
The Cherno
50 Game Programming - Episode 9 - Rendering Pixels
Game Programming - Episode 9 - Rendering Pixels
The Cherno
51 Game Programming - Episode 10 - Clearing the Screen
Game Programming - Episode 10 - Clearing the Screen
The Cherno
52 Game Programming - Episode 11 - "Out of Bounds, Baby!"
Game Programming - Episode 11 - "Out of Bounds, Baby!"
The Cherno
53 Game Programming - Episode 12 - Negative Bounds
Game Programming - Episode 12 - Negative Bounds
The Cherno
54 Game Programming - Episode 13 - Timer
Game Programming - Episode 13 - Timer
The Cherno
55 Game Programming - Episode 14 - FPS Counter
Game Programming - Episode 14 - FPS Counter
The Cherno
56 Episode 15 - Tiles
Episode 15 - Tiles
The Cherno
57 Game Programming - Episode 16 - The Map
Game Programming - Episode 16 - The Map
The Cherno
58 The Walls 2 - Minecraft PvP Survival Map
The Walls 2 - Minecraft PvP Survival Map
The Cherno
59 Game Programming - Episode 17 - Key Input
Game Programming - Episode 17 - Key Input
The Cherno
60 Game Programming - Episode 18 - Controlling The Map
Game Programming - Episode 18 - Controlling The Map
The Cherno

This video teaches the basics of RAG search and compares the graphics pipeline and compute pipeline for image rendering, demonstrating how to optimize the compute pipeline for better performance. It provides a hands-on approach to computer science and explores different approaches to optimizing pipeline performance.

Key Takeaways
  1. Update the GPU compute repository
  2. Add create graphics shader and reload graphics shader functions to the shader class
  3. Create a vertex array and vertex buffer
  4. Create an index buffer
  5. Draw a quad using a triangle strip with four indices
  6. Use visually debug to spot errors in shader code
  7. Use triangle approach instead of quad approach for optimization
  8. Copy code from a compute shader to a fragment shader and optimize it
  9. Use normalized coordinates and texture size to simplify the code
  10. Bind textures at same binding points for graphics and compute pipelines
💡 The compute pipeline can be optimized for better performance by reducing unnecessary calculations and using normalized coordinates and texture size.

Related Reads

Chapters (5)

Intro
3:30 Rendering a fullscreen quad/triangle
21:21 Compute to fragment shader porting
28:25 Optimizing our compute shader
30:24 Final results
Up next
Stump Grinder Carbide Wheel Grinds Hardwood To Chips
Innoforge Studio
Watch →