Everything you have ever seen drawn on a screen in a game happened because some CPU program called a graphics API, over and over, many times a frame, telling a GPU exactly what to draw and how. In Unity you never see these calls — the engine makes them for you. This chapter opens that hood. You will first learn OpenGL, the simplest of the three real graphics APIs, by building a working triangle from raw pieces: a buffer of vertex data, a description of that data's layout, two small compiled programs called shaders, and one function call that finally draws something. Then you will see why the industry mostly moved on from OpenGL to Vulkan and DirectX 12, and learn the shape of those two APIs — the shape that shows up in real engine job interviews.
Your game's code runs on the CPU. The actual pixels on screen are produced by the GPU (Graphics Processing Unit — a separate processor built with thousands of small cores, designed to do the same simple math on huge amounts of data at once, instead of a few complex things one at a time like a CPU core does). Your CPU code cannot directly poke GPU memory or tell GPU cores what to run. It needs an agreed-upon contract for the two to talk. That contract is a graphics API (Application Programming Interface — a fixed set of functions and rules two pieces of software agree to use to communicate).
When you call a graphics API function, you are not talking to the GPU chip directly. You are talking to the driver (a piece of software, written by the GPU vendor — NVIDIA, AMD, Intel, Apple, Qualcomm — that translates your API calls into the actual machine instructions that specific GPU understands). Different GPUs from different vendors have wildly different hardware designs underneath, but they can all expose the same OpenGL, Vulkan, or DirectX 12 functions, because the driver is the piece that hides the hardware differences.
There are three graphics APIs that matter for a game programming career today:
This chapter teaches the core objects using OpenGL first, because its function calls map most directly onto the underlying ideas (a buffer, a shader, a texture, a draw call) without extra ceremony. Once those ideas are solid, Vulkan and DirectX 12 stop looking like alien languages — they are the same ideas, done explicitly instead of automatically.
All three APIs exist to do the same thing: get vertex data and shader programs onto the GPU, and issue draw calls that turn triangles into colored pixels. The difference is how much work the driver silently does for you versus how much you must spell out yourself.
OpenGL is a state machine (a system that remembers a current set of settings — "the currently bound buffer," "the currently active shader program" — and every function call reads or changes those remembered settings, instead of you passing every parameter explicitly every time). This makes OpenGL short to write, but it also means the driver has to guess a lot: it re-validates state on nearly every call, and it manages memory, threading, and synchronization for you behind the scenes. That hidden work costs CPU time on every single draw call.
Vulkan and DirectX 12 remove almost all of that hidden work. You describe your rendering setup once, in detail, up front — and the driver validates it once, not every frame. You manage your own GPU memory. You record work from multiple CPU threads at the same time. And critically, you are responsible for telling the GPU exactly when one piece of work must wait for another, instead of the driver quietly inserting waits "to be safe." Section 9 explains why this trade-off became worth making.
Before writing any code, it helps to see the whole picture. OpenGL work is built from a handful of object types, and you connect them by binding (making an object "current," so that later function calls implicitly act on it). Once you can see this graph in your head, every OpenGL function call becomes "which object am I binding, and what am I telling it."
Five kinds of objects, five jobs:
The next five sections build each of these, in code, one at a time, before Section 8 wires them all together into one working program.
A vertex is one corner point of the shape you are drawing — it usually carries a position, and often other data too (a color, a texture coordinate, a normal direction). A triangle needs three vertices. Before the GPU can draw anything, that vertex data has to live in GPU memory, not just CPU RAM — the GPU cannot read your program's regular heap or stack directly.
float vertices[] = {
// x y z
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
You already know & from the C chapters — it takes the address of a variable. glGenVertexArrays(1, &vao) asks OpenGL to generate 1 new VAO name and write its ID straight into your vao variable, through that pointer, instead of returning a value. Almost every OpenGL "create" function follows this same shape.
Walking through the rest, call by call:
glBindVertexArray(vao) — makes vao the "current" VAO. Every attribute-related call below now writes into this VAO's memory of how to read data.glBindBuffer(GL_ARRAY_BUFFER, vbo) — makes vbo the current buffer for the GL_ARRAY_BUFFER target (OpenGL's state machine again: this is a "current buffer for this purpose" slot, not a direct reference).glBufferData(...) — copies sizeof(vertices) bytes from your CPU array into the bound buffer's GPU memory. GL_STATIC_DRAW is a hint to the driver: "I will upload this once and draw it many times," which lets the driver choose faster memory for it.glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0) — describes how to read attribute slot 0: 3 floats per vertex, not normalized, 3 floats (12 bytes) apart from one vertex to the next (the stride), starting at byte offset 0.glEnableVertexAttribArray(0) — turns attribute slot 0 on. Without this call, the GPU ignores the buffer for that slot entirely.glVertexAttribPointer and glEnableVertexAttribArray without a VAO bound, or forgetting glEnableVertexAttribArray entirely. If the attribute is never enabled, the GPU reads a constant default value (0, 0, 0, 1) for every vertex instead of your buffer's data — every vertex collapses onto the same point, and a zero-area triangle produces nothing visible. This exact bug is Exercise 1 below.Three vertices are enough for a triangle, but real meshes reuse vertices — a quad's two triangles share two corners. Instead of duplicating those shared vertices, you can store each unique vertex once and add an EBO (Element Buffer Object, also called an index buffer): a list of integers saying which vertices, in which order, form each triangle.
unsigned int indices[] = { 0, 1, 2 }; // triangle uses vertices 0, 1, 2
unsigned int ebo;
glGenBuffers(1, &ebo);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
// later, draw through the index buffer instead of glDrawArrays:
glDrawElements(GL_TRIANGLES, 3, GL_UNSIGNED_INT, 0);
An EBO stays bound to whichever VAO was bound when you called glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ...), exactly like the VBO's attribute layout does — that is the whole point of the VAO: it bundles a VBO's layout and an EBO together so binding one VAO at draw time restores the entire setup.
A shader is a small program that runs directly on the GPU, written in GLSL (OpenGL Shading Language — a C-like language for GPU programs). Two shader stages are required for basic drawing:
gl_Position.
const char* vertexSrc =
"#version 330 core\n"
"layout (location = 0) in vec3 aPos;\n"
"void main() {\n"
" gl_Position = vec4(aPos, 1.0);\n"
"}\n";
const char* fragmentSrc =
"#version 330 core\n"
"out vec4 FragColor;\n"
"void main() {\n"
" FragColor = vec4(1.0, 0.5, 0.2, 1.0);\n"
"}\n";
layout (location = 0) in vec3 aPos in the vertex shader is the other end of attribute slot 0 from Section 4 — that number 0 is not a coincidence, it is how the vertex buffer's bytes become the aPos variable inside GLSL. The fragment shader here ignores whatever the vertex shader passes it and always outputs a fixed orange color, vec4(1.0, 0.5, 0.2, 1.0) (red, green, blue, alpha).
GLSL source strings are just text — they are not compiled until you hand them to the driver at runtime. Compiling and checking for errors follows the same pattern for every shader:
unsigned int CompileShader(unsigned int type, const char* src)
{
unsigned int shader = glCreateShader(type);
glShaderSource(shader, 1, &src, nullptr);
glCompileShader(shader);
int success;
glGetShaderiv(shader, GL_COMPILE_STATUS, &success);
if (!success)
{
char log[512];
glGetShaderInfoLog(shader, 512, nullptr, log);
printf("Shader compile error: %s\n", log);
}
return shader;
}
Worked trace: if vertexSrc had a typo — say, a missing semicolon after gl_Position = vec4(aPos, 1.0) — glCompileShader would fail silently (it does not throw or crash), success would come back as 0, and the console would print something like Shader compile error: 0:4(5): error: syntax error, unexpected '}', straight from the driver's own GLSL compiler.
Once both shaders compile, link them into one shader program — the object you actually bind and draw with:
unsigned int vs = CompileShader(GL_VERTEX_SHADER, vertexSrc);
unsigned int fs = CompileShader(GL_FRAGMENT_SHADER, fragmentSrc);
unsigned int program = glCreateProgram();
glAttachShader(program, vs);
glAttachShader(program, fs);
glLinkProgram(program);
int linked;
glGetProgramiv(program, GL_LINK_STATUS, &linked);
if (!linked)
{
char log[512];
glGetProgramInfoLog(program, 512, nullptr, log);
printf("Program link error: %s\n", log);
}
glDeleteShader(vs); // the compiled shader objects are copied into
glDeleteShader(fs); // the program at link time; the originals are no longer needed
GL_COMPILE_STATUS and GL_LINK_STATUS, even in a throwaway test program. A shader that fails to compile does not crash your game — it just means program is unusable, and every following draw call silently draws nothing (or draws using whatever the last successfully-bound program was). A black or blank window with no error printed anywhere is the single most common early OpenGL bug, and it is almost always an unchecked shader error.A texture is an image stored in GPU memory that a shader can sample — look up a color from, usually with filtering that blends nearby pixels smoothly. Setting one up follows the same generate-bind-configure-upload shape as buffers:
unsigned int texture;
glGenTextures(1, &texture);
glBindTexture(GL_TEXTURE_2D, texture);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR_MIPMAP_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
// width, height, and pixels already loaded from an image file (e.g. PNG)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, pixels);
glGenerateMipmap(GL_TEXTURE_2D);
The GL_TEXTURE_WRAP_S / GL_TEXTURE_WRAP_T parameters control what happens when a texture coordinate goes outside the 0 to 1 range (GL_REPEAT tiles it). The filter parameters control how a pixel between texture samples is computed. glGenerateMipmap builds a chain of progressively smaller, pre-blurred copies of the texture — a mipmap chain — so that a texture shown small and far away samples from an already-shrunk copy instead of aliasing and shimmering.
Inside a fragment shader, you sample a bound texture with a sampler2D and the built-in texture() function:
#version 330 core
in vec2 vUV;
out vec4 FragColor;
uniform sampler2D uTex;
void main()
{
FragColor = texture(uTex, vUV);
}
vUV here is a texture coordinate passed in from the vertex shader (interpolated smoothly across the triangle by the rasterizer for each fragment), and uTex is a uniform (a value set once from CPU code that stays the same for every vertex or fragment in a draw call, unlike an attribute which differs per vertex).
Every draw call writes color (and usually depth) into a framebuffer. By default, that is the window itself — the default framebuffer, managed for you, swapped to the screen once per frame. But you can also create your own framebuffer object (FBO) that renders into a texture instead of the screen — the basis of post-processing effects, shadow maps, mirrors, and render-to-texture UI.
unsigned int fbo;
glGenFramebuffers(1, &fbo);
glBindFramebuffer(GL_FRAMEBUFFER, fbo);
unsigned int colorTex;
glGenTextures(1, &colorTex);
glBindTexture(GL_TEXTURE_2D, colorTex);
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, 800, 600, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, colorTex, 0);
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE)
{
printf("Framebuffer is not complete!\n");
}
glBindFramebuffer(GL_FRAMEBUFFER, 0); // back to the window
glTexImage2D here passes nullptr for the pixel data — this texture starts empty; it is a destination to render into, not an image to read from a file. glFramebufferTexture2D attaches that empty texture as the framebuffer's color output. From this point, anything drawn while fbo is bound lands in colorTex instead of on screen. A second pass later binds the default framebuffer (0) and draws a full-screen quad that samples colorTex as an ordinary texture, applying whatever post-processing effect the fragment shader implements.
glBindFramebuffer(GL_FRAMEBUFFER, 0) after finishing an off-screen pass. Every draw call after that keeps landing in the off-screen texture instead of the window, so the screen appears to stop updating (it is frozen on whatever was last drawn to the default framebuffer) while rendering otherwise looks like it is working fine.Every piece is now in place. A draw call is the function call that actually tells the GPU "run the currently bound program over the currently bound vertex data, now." Here is the full setup and render loop, combining Sections 4 and 5 (window and OpenGL context creation through a library like GLFW is left out — that part is windowing boilerplate, not OpenGL itself):
// Assume `window` already exists and a valid OpenGL context is current.
float vertices[] = {
-0.5f, -0.5f, 0.0f,
0.5f, -0.5f, 0.0f,
0.0f, 0.5f, 0.0f
};
unsigned int vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
unsigned int program = BuildProgram(vertexSrc, fragmentSrc); // Section 5's two functions combined
while (!glfwWindowShouldClose(window))
{
glClearColor(0.1f, 0.1f, 0.12f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
glUseProgram(program);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3); // 3 vertices, starting at index 0
glfwSwapBuffers(window);
glfwPollEvents();
}
Expected result: a window opens with a dark bluish-gray background, and a solid orange triangle appears in the middle of it, pointing upward — its three corners exactly matching the three (x, y) pairs in vertices, mapped from OpenGL's -1 to 1 coordinate range onto the window's pixels.
Notice what glDrawArrays(GL_TRIANGLES, 0, 3) alone does not say: which buffer to read (that came from the bound VAO), which shaders to run (that came from the bound program), where to write pixels (that came from the bound framebuffer). Every OpenGL draw call is short precisely because it silently reuses whatever is currently bound — which is convenient to write and exactly the hidden state Section 2 mentioned as OpenGL's trade-off.
OpenGL's convenience is not free. Every gl* call is checked and validated by the driver, state is tracked and re-verified on nearly every call, and — critically — the entire OpenGL context is effectively single-threaded: calls from other CPU threads mostly funnel through internal locks in the driver. For years this was fine, because GPUs were the bottleneck, not CPUs issuing draw calls.
That changed. Modern games issue thousands of draw calls per frame, and modern CPUs have many cores sitting idle while OpenGL forces almost all API traffic through one thread. Measured on real games, a meaningful chunk of a frame's CPU time could go to nothing but the driver re-validating state and translating calls it had already seen the frame before.
Vulkan (2016) and DirectX 12 (2015), alongside Apple's Metal, were designed around the same set of fixes:
The cost is verbosity and responsibility. A minimal "hello triangle" that is roughly 80 lines in OpenGL is commonly 800 to 1,000+ lines in Vulkan, because work that the OpenGL driver used to do silently — picking a GPU, managing memory, tracking when it is safe to reuse a buffer — is now explicitly your job. This is exactly why studios building or maintaining a serious in-house engine hire specifically for Vulkan/DirectX 12 experience: getting this right, and getting it fast, is a real, specialized skill.
Vulkan replaces OpenGL's single implicit context with an explicit hierarchy of objects you create yourself, in order:
Working through that hierarchy in code (structure fields trimmed for length — a real version fills in application info, required extensions, and validation layers):
VkInstanceCreateInfo instanceInfo{};
instanceInfo.sType = VK_STRUCTURE_TYPE_INSTANCE_CREATE_INFO;
VkInstance instance;
vkCreateInstance(&instanceInfo, nullptr, &instance);
uint32_t gpuCount = 0;
vkEnumeratePhysicalDevices(instance, &gpuCount, nullptr);
std::vector<VkPhysicalDevice> gpus(gpuCount);
vkEnumeratePhysicalDevices(instance, &gpuCount, gpus.data());
VkPhysicalDevice physicalDevice = gpus[0]; // real code scores and picks the best one
VkDeviceCreateInfo deviceInfo{};
deviceInfo.sType = VK_STRUCTURE_TYPE_DEVICE_CREATE_INFO;
VkDevice device;
vkCreateDevice(physicalDevice, &deviceInfo, nullptr, &device);
VkQueue graphicsQueue;
vkGetDeviceQueue(device, graphicsQueueFamilyIndex, 0, &graphicsQueue);
Each piece has one job:
vkEnumeratePhysicalDevices is called twice on purpose, in a pattern used all over Vulkan — once with a null output pointer to ask "how many are there," once with a sized array to actually receive them.In OpenGL, calling a function like glDrawArrays effectively runs immediately (or close to it). Vulkan and DirectX 12 split that in two: first you record a sequence of commands into a command buffer — nothing on the GPU happens yet, you are just building a list — then you submit that whole buffer to a queue, and the GPU executes it whenever it gets to it, possibly overlapped with other submitted work.
vkBeginCommandBuffer(cmd, &beginInfo);
VkRenderPassBeginInfo rpInfo{};
rpInfo.sType = VK_STRUCTURE_TYPE_RENDER_PASS_BEGIN_INFO;
rpInfo.renderPass = renderPass;
rpInfo.framebuffer = swapchainFramebuffers[imageIndex];
rpInfo.renderArea.extent = swapchainExtent;
rpInfo.clearValueCount = 1;
rpInfo.pClearValues = &clearColor;
vkCmdBeginRenderPass(cmd, &rpInfo, VK_SUBPASS_CONTENTS_INLINE);
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, trianglePipeline);
vkCmdBindVertexBuffers(cmd, 0, 1, &vertexBuffer, &offset);
vkCmdDraw(cmd, 3, 1, 0, 0); // 3 vertices, 1 instance, starting at vertex 0, instance 0
vkCmdEndRenderPass(cmd);
vkEndCommandBuffer(cmd);
Every vkCmd* function here only appends an instruction to cmd's recorded list — none of them touch the GPU directly. Submitting is a separate, explicit step:
VkSubmitInfo submitInfo{};
submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO;
submitInfo.waitSemaphoreCount = 1;
submitInfo.pWaitSemaphores = &imageAvailableSemaphore;
submitInfo.pWaitDstStageMask = &waitStage;
submitInfo.commandBufferCount = 1;
submitInfo.pCommandBuffers = &cmd;
submitInfo.signalSemaphoreCount = 1;
submitInfo.pSignalSemaphores = &renderFinishedSemaphore;
vkQueueSubmit(graphicsQueue, 1, &submitInfo, inFlightFence);
The waitSemaphoreCount/signalSemaphoreCount fields on the submit are exactly the synchronization Section 9 promised you would have to spell out yourself. That is the subject of the next section.
GPU work is asynchronous (it runs independently of, and possibly much later than, the CPU code that submitted it). The CPU can submit a command buffer and immediately move on to recording the next frame, while the GPU is still working through the previous one. That is exactly the speed Vulkan is built for — but it means nothing stops you from, say, overwriting a buffer the GPU has not finished reading yet, or sampling a texture before an earlier pass has finished writing to it. In OpenGL the driver quietly inserted waits to prevent this. In Vulkan and DirectX 12, if you do not say so, it does not happen — you get a data race: flickering, garbage pixels, or a validation-layer error during development.
Three tools handle three different kinds of waiting:
VkImageMemoryBarrier barrier{};
barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER;
barrier.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED;
barrier.newLayout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL;
barrier.srcAccessMask = 0;
barrier.dstAccessMask = VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT;
barrier.image = colorImage;
barrier.subresourceRange = { VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1 };
vkCmdPipelineBarrier(
cmd,
VK_PIPELINE_STAGE_TOP_OF_PIPE_BIT, // wait for nothing before this
VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT, // block this stage until satisfied
0,
0, nullptr,
0, nullptr,
1, &barrier);
DirectX 12 was built around the same explicit philosophy as Vulkan, released around the same time, for the same reasons described in Section 9. If you understand the Vulkan model from Sections 10 to 12, DirectX 12 is mostly a vocabulary problem:
The one concept worth slowing down on is the root signature. In Vulkan, telling a shader what resources it can access is split across a pipeline layout and one or more descriptor set layouts. DirectX 12 combines this into a single root signature (ID3D12RootSignature) — think of it as a function signature for the whole pipeline: it declares "this pipeline's shaders expect a constant buffer at register b0, a texture and sampler at register t0/s0," and so on. At draw time, you bind actual descriptor tables (groups of resource references living in a descriptor heap, ID3D12DescriptorHeap) that match that declared shape.
ID3D12CommandQueue* queue;
D3D12_COMMAND_QUEUE_DESC queueDesc = {};
queueDesc.Type = D3D12_COMMAND_LIST_TYPE_DIRECT;
device->CreateCommandQueue(&queueDesc, IID_PPV_ARGS(&queue));
commandList->SetGraphicsRootSignature(rootSignature);
commandList->SetPipelineState(pipelineState); // the PSO: shaders + blend/depth/raster state, baked
commandList->IASetVertexBuffers(0, 1, &vertexBufferView);
commandList->DrawInstanced(3, 1, 0, 0); // 3 vertices, 1 instance
commandList->Close();
ID3D12CommandList* lists[] = { commandList };
queue->ExecuteCommandLists(1, lists);
Notice the shape is identical to Vulkan's: build a pipeline object once (the PSO), record commands into a list, close the list, and hand it to a queue to execute — the industry converged on this same explicit pattern in Vulkan, DirectX 12, and Metal at roughly the same time, because they were all solving the same CPU-overhead problem.
unsigned int vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glBindVertexArray(0);
// ... later, in the render loop ...
glUseProgram(program);
glBindVertexArray(vao);
glDrawArrays(GL_TRIANGLES, 0, 3);
The missing call is glEnableVertexAttribArray(0). glVertexAttribPointer only describes how attribute slot 0 would be read from the buffer — it does not turn that slot on. Every vertex attribute slot starts disabled. With slot 0 disabled, the vertex shader's aPos input reads a fixed default value, (0, 0, 0, 1), for every single vertex, no matter what is actually sitting in the VBO. All three vertices of the triangle collapse onto the exact same point at the origin, the triangle has zero area, and the rasterizer produces no fragments for it — so the fragment shader never runs, and nothing is drawn. The screen still clears normally because glClear does not depend on the broken triangle at all.
unsigned int vao, vbo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0); // added: the fix
glBindVertexArray(0);
glDrawElements. The four corners are: (-0.5, -0.5, 0), (0.5, -0.5, 0), (0.5, 0.5, 0), (-0.5, 0.5, 0). The two triangles should be: (0, 1, 2) and (2, 3, 0).
float vertices[] = {
-0.5f, -0.5f, 0.0f, // 0: bottom-left
0.5f, -0.5f, 0.0f, // 1: bottom-right
0.5f, 0.5f, 0.0f, // 2: top-right
-0.5f, 0.5f, 0.0f // 3: top-left
};
unsigned int indices[] = {
0, 1, 2, // first triangle
2, 3, 0 // second triangle
};
unsigned int vao, vbo, ebo;
glGenVertexArrays(1, &vao);
glGenBuffers(1, &vbo);
glGenBuffers(1, &ebo);
glBindVertexArray(vao);
glBindBuffer(GL_ARRAY_BUFFER, vbo);
glBufferData(GL_ARRAY_BUFFER, sizeof(vertices), vertices, GL_STATIC_DRAW);
glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, ebo);
glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(indices), indices, GL_STATIC_DRAW);
glVertexAttribPointer(0, 3, GL_FLOAT, GL_FALSE, 3 * sizeof(float), (void*)0);
glEnableVertexAttribArray(0);
glBindVertexArray(0);
// in the render loop:
glUseProgram(program);
glBindVertexArray(vao);
glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_INT, 0); // 6 indices total
Only 4 vertices are stored, even though 2 triangles need 6 corners between them — vertices 0 and 2 are each reused by both triangles through the index buffer instead of being written twice into vertices[]. The EBO is bound while vao is bound, so the VAO remembers it too, and a single glBindVertexArray(vao) at draw time restores the vertex layout, the VBO, and the EBO all together. glDrawElements's second argument, 6, is the number of indices to read, not the number of unique vertices.
(a) Acquiring the swapchain image: a semaphore (commonly named imageAvailableSemaphore), signaled by vkAcquireNextImageKHR. Only Pass B needs to wait on it, since Pass B is the one that writes into the swapchain image — Pass A only touches its own off-screen texture and does not need the swapchain image at all, so it could in principle start recording or even executing before the swapchain image is ready.
(b) Between Pass A and Pass B: a pipeline (image memory) barrier on the off-screen color texture, transitioning it from COLOR_ATTACHMENT_OPTIMAL (the layout used while Pass A is writing to it) to SHADER_READ_ONLY_OPTIMAL (the layout Pass B's fragment shader needs to sample it), with a memory dependency ensuring Pass A's color writes are complete and visible before Pass B's shader reads. If both passes are recorded into the same command buffer this is one vkCmdPipelineBarrier call between them; if they are in separate command buffers submitted separately, the same guarantee has to come from how those submissions are ordered and synchronized.
(c) Pass B finishing and presenting: a semaphore (commonly renderFinishedSemaphore), signaled when Pass B's command buffer finishes execution on the GPU, which vkQueuePresentKHR waits on before the image is actually shown. Separately, a fence for that frame (commonly inFlightFence) is needed so the CPU knows when it is finally safe to reuse this frame's command buffer and resources — typically checked before the CPU starts recording the same frame-in-flight slot again, two or three frames later.