<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Remo H. Jansen</title>
    <description>The latest articles on Forem by Remo H. Jansen (@remojansen).</description>
    <link>https://forem.com/remojansen</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F24875%2F4cfbc41a-3739-44c3-937c-9a9e3b231097.jpeg</url>
      <title>Forem: Remo H. Jansen</title>
      <link>https://forem.com/remojansen</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/remojansen"/>
    <language>en</language>
    <item>
      <title>Building a 3D engine from scratch with C++ and Vulkan for web developers Part II: Rendering your first triangle</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sun, 17 May 2026 12:05:53 +0000</pubDate>
      <link>https://forem.com/remojansen/building-a-3d-engine-from-scratch-with-c-and-vulkan-for-web-developers-part-ii-rendering-your-464b</link>
      <guid>https://forem.com/remojansen/building-a-3d-engine-from-scratch-with-c-and-vulkan-for-web-developers-part-ii-rendering-your-464b</guid>
      <description>&lt;h2&gt;
  
  
  Where we left off
&lt;/h2&gt;

&lt;p&gt;In &lt;a href="https://dev.to/remojansen/building-a-3d-engine-from-scratch-with-c-and-vulkan-for-web-developers-part-i-bootstrapping-1bap"&gt;Part I&lt;/a&gt;, we set up the Vulkan infrastructure: instance, window, surface, device, and swapchain. We have a GPU ready to receive commands, and a queue of images waiting to be drawn to and displayed on screen.&lt;/p&gt;

&lt;p&gt;But at that point we didn't draw anything. The game loop just polled for window events and looped. Now we're going to make pixels appear — specifically, a colored triangle. This is the "Hello, World!" of graphics programming. Every GPU tutorial starts here because a triangle is the simplest shape the GPU can draw, and getting one on screen proves the entire pipeline is working.&lt;/p&gt;

&lt;p&gt;Here's what we need to build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Swapchain (Part I) → Render Pass → Shaders → Pipeline → Command Pool → Framebuffers → Sync → Draw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these builds on the previous one. By the end, we'll have a working render loop that clears the screen to black, draws a rainbow triangle, and presents the result every frame.&lt;/p&gt;

&lt;h2&gt;
  
  
  The updated architecture
&lt;/h2&gt;

&lt;p&gt;The renderer has grown. Here's the directory structure now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/engine/renderer/
├── instance         ← Vulkan runtime (Part I)
├── device           ← GPU selection (Part I)
├── swapchain        ← image buffers for presenting frames (Part I)
├── render_pass      ← describes what we render to and how     ← NEW
├── shader           ← loads compiled GPU programs             ← NEW
├── pipeline         ← the full GPU configuration for drawing  ← NEW
├── command_pool     ← allocates command buffers               ← NEW
└── renderer         ← orchestrates everything                 ← UPDATED
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the shader source files live in a new location:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/engine/assets/shaders/
├── triangle.vert    ← vertex shader (positions + colors)
├── triangle.frag    ← fragment shader (pixel colors)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Render Pass
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/render_pass.h" rel="noopener noreferrer"&gt;Source: render_pass.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/render_pass.cpp" rel="noopener noreferrer"&gt;render_pass.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A render pass describes &lt;em&gt;where&lt;/em&gt; you're rendering to and &lt;em&gt;how&lt;/em&gt; the output should be treated. It doesn't draw anything itself — it's a blueprint that tells Vulkan: "when I render a frame, I'll be writing to this kind of image, I want you to clear it first, and when I'm done, the image should be ready to display on screen."&lt;/p&gt;

&lt;p&gt;If you're coming from web development, think of it as configuring a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; context. When you call &lt;code&gt;canvas.getContext('2d')&lt;/code&gt; or &lt;code&gt;canvas.getContext('webgl')&lt;/code&gt;, you're telling the browser what kind of rendering you'll do and what the output format looks like. A render pass is the Vulkan version of that — except far more explicit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;RenderPass&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkRenderPass&lt;/span&gt; &lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;RenderPass&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;RenderPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VkFormat&lt;/span&gt; &lt;span class="n"&gt;swapchainFormat&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a render pass involves three pieces:&lt;/p&gt;

&lt;h3&gt;
  
  
  Attachments
&lt;/h3&gt;

&lt;p&gt;An &lt;strong&gt;attachment&lt;/strong&gt; is the image you're rendering into — in our case, the swapchain image. We describe it with &lt;code&gt;VkAttachmentDescription&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkAttachmentDescription&lt;/span&gt; &lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swapchainFormat&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_SAMPLE_COUNT_1_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loadOp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_ATTACHMENT_LOAD_OP_CLEAR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;storeOp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_ATTACHMENT_STORE_OP_STORE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;initialLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_IMAGE_LAYOUT_UNDEFINED&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;finalLayout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_IMAGE_LAYOUT_PRESENT_SRC_KHR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;loadOp = CLEAR&lt;/code&gt;&lt;/strong&gt; — when the render pass begins, clear the image (to black, or whatever clear color we set). This is like calling &lt;code&gt;ctx.clearRect(0, 0, width, height)&lt;/code&gt; at the start of each frame in a canvas game.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;storeOp = STORE&lt;/code&gt;&lt;/strong&gt; — when the render pass ends, keep the rendered result. (The alternative, &lt;code&gt;DONT_CARE&lt;/code&gt;, would let the driver discard it — useful for temporary buffers, not for something we want to display.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;initialLayout = UNDEFINED&lt;/code&gt;&lt;/strong&gt; — we don't care what state the image is in before we start. We're going to clear it anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;finalLayout = PRESENT_SRC_KHR&lt;/code&gt;&lt;/strong&gt; — when the render pass finishes, the image should be in a layout ready for presentation. Vulkan images can be in different "layouts" optimized for different operations (rendering, reading, presenting). This tells the driver to transition the image to a presentation-ready layout when we're done.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Subpasses
&lt;/h3&gt;

&lt;p&gt;A render pass can contain multiple &lt;strong&gt;subpasses&lt;/strong&gt; — stages that render to different combinations of attachments. Think of it like a multi-pass compositor: one pass renders the scene, another applies post-processing, another adds UI. Each subpass can read the output of the previous one.&lt;/p&gt;

&lt;p&gt;For our triangle, we only need one subpass. It writes to a single color attachment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkAttachmentReference&lt;/span&gt; &lt;span class="n"&gt;colorRef&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;colorRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attachment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorRef&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;VkSubpassDescription&lt;/span&gt; &lt;span class="n"&gt;subpass&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;subpass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipelineBindPoint&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_PIPELINE_BIND_POINT_GRAPHICS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;subpass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colorAttachmentCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;subpass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pColorAttachments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;colorRef&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we explain the fields, it helps to understand how &lt;code&gt;VkAttachmentDescription&lt;/code&gt; and &lt;code&gt;VkAttachmentReference&lt;/code&gt; relate. The description defines &lt;em&gt;what&lt;/em&gt; an attachment is — its format, how it's loaded and stored, and what layouts it transitions through. The reference is a &lt;em&gt;pointer&lt;/em&gt; to one of those descriptions by index. Think of the render pass as having an array of attachment descriptions; subpasses refer to them by index rather than duplicating the definition.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;attachment = 0&lt;/code&gt;&lt;/strong&gt; — index into the render pass's attachment descriptions array. We only have one attachment (the color attachment we described above), so it's index 0.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layout = VK_IMAGE_LAYOUT_COLOR_ATTACHMENT_OPTIMAL&lt;/code&gt;&lt;/strong&gt; — the image layout to use &lt;em&gt;during&lt;/em&gt; this subpass. This tells the driver to keep the image in a layout optimized for writing color data. This is different from &lt;code&gt;initialLayout&lt;/code&gt;/&lt;code&gt;finalLayout&lt;/code&gt; on the description, which control the layouts &lt;em&gt;before&lt;/em&gt; and &lt;em&gt;after&lt;/em&gt; the entire render pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pipelineBindPoint = VK_PIPELINE_BIND_POINT_GRAPHICS&lt;/code&gt;&lt;/strong&gt; — this subpass uses the graphics pipeline (as opposed to &lt;code&gt;VK_PIPELINE_BIND_POINT_COMPUTE&lt;/code&gt; for compute shaders). It determines which type of pipeline can be bound during this subpass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;colorAttachmentCount = 1&lt;/code&gt;&lt;/strong&gt; — how many color attachments this subpass writes to. A subpass can write to multiple render targets simultaneously (called MRT — multiple render targets), but we only need one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;pColorAttachments = &amp;amp;colorRef&lt;/code&gt;&lt;/strong&gt; — pointer to the array of attachment references. The index in this array matters: &lt;code&gt;layout(location = 0) out vec4 outColor&lt;/code&gt; in the fragment shader writes to &lt;code&gt;pColorAttachments[0]&lt;/code&gt;. If you had two color attachments, &lt;code&gt;location = 1&lt;/code&gt; would write to &lt;code&gt;pColorAttachments[1]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Dependencies
&lt;/h3&gt;

&lt;p&gt;The &lt;strong&gt;subpass dependency&lt;/strong&gt; is synchronization. It says: "don't start writing color until the swapchain image is actually available." Without this, the GPU might try to render into an image that's still being displayed on screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkSubpassDependency&lt;/span&gt; &lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcSubpass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_SUBPASS_EXTERNAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dstSubpass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;srcStageMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dstStageMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_PIPELINE_STAGE_COLOR_ATTACHMENT_OUTPUT_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;dependency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dstAccessMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_ACCESS_COLOR_ATTACHMENT_WRITE_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;VK_SUBPASS_EXTERNAL&lt;/code&gt; means "everything that happened before this render pass" — in our case, acquiring the swapchain image. The dependency ensures that our color writing waits until that acquisition is complete. If you've ever dealt with race conditions in async JavaScript, this is the same concept — but at the GPU hardware level.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Shaders
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/shader.h" rel="noopener noreferrer"&gt;Source: shader.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/shader.cpp" rel="noopener noreferrer"&gt;shader.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Shaders are programs that run on the GPU. If C++ code runs on the CPU, shaders are the code that runs on the GPU's thousands of tiny cores — in parallel, for every vertex and every pixel, every frame.&lt;/p&gt;

&lt;p&gt;In web development, the closest equivalent is WebGL shaders. If you've ever written GLSL inside a &lt;code&gt;&amp;lt;script type="x-shader/x-vertex"&amp;gt;&lt;/code&gt; tag or passed shader strings to &lt;code&gt;gl.shaderSource()&lt;/code&gt;, that's the same language we're using here. The key difference is the compilation model.&lt;/p&gt;

&lt;h3&gt;
  
  
  The two shaders
&lt;/h3&gt;

&lt;p&gt;Every graphics program needs at least two shaders:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertex shader&lt;/strong&gt; — runs once per vertex. Its job is to determine &lt;em&gt;where&lt;/em&gt; each vertex appears on screen. Think of it as a function that transforms 3D world coordinates into 2D screen coordinates.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a vertex?&lt;/strong&gt; A vertex is a point in space — a corner of a shape. A triangle has 3 vertices, a cube has 8. Each vertex has a position (x, y, z coordinates), and can carry extra data like color or texture coordinates. Every 3D model you've ever seen in a game is made of thousands of triangles, and every triangle is defined by exactly 3 vertices.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Fragment shader&lt;/strong&gt; — runs once per pixel (technically, per fragment — a candidate pixel). Its job is to determine &lt;em&gt;what color&lt;/em&gt; each pixel should be. Think of it as a function that takes a pixel's position and produces an RGBA color.&lt;/p&gt;

&lt;p&gt;Here's our vertex shader:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 450
&lt;/span&gt;
&lt;span class="kt"&gt;vec2&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;[](&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="kt"&gt;vec2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;[](&lt;/span&gt;
    &lt;span class="kt"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// red&lt;/span&gt;
    &lt;span class="kt"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;   &lt;span class="c1"&gt;// green&lt;/span&gt;
    &lt;span class="kt"&gt;vec3&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// blue&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;fragColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;gl_Position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gl_VertexIndex&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;fragColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;colors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;gl_VertexIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things to unpack:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The positions are hardcoded in the shader.&lt;/strong&gt; Normally you'd send vertex data from the CPU via a vertex buffer (we'll do that in a later part). For now, the triangle's three corners are baked directly into the shader code. The coordinates are in &lt;strong&gt;clip space&lt;/strong&gt;: X goes from -1 (left) to 1 (right), Y goes from -1 (top) to 1 (bottom), and the center is (0, 0). Our triangle spans from the top-center to the bottom-left and bottom-right.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is clip space?&lt;/strong&gt; Clip space is the coordinate system the vertex shader outputs to. It's a normalized box where X and Y range from -1 to 1, regardless of your window's actual pixel size. The GPU later maps these coordinates to real pixels based on the viewport dimensions. Think of it like using percentages in CSS instead of pixels — you describe positions relative to the available space, not in absolute units.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gl_VertexIndex&lt;/code&gt;&lt;/strong&gt; is a built-in variable that tells the shader which vertex it's currently processing (0, 1, or 2). It's like the &lt;code&gt;index&lt;/code&gt; parameter in &lt;code&gt;array.map((item, index) =&amp;gt; ...)&lt;/code&gt; — it lets you look up per-vertex data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;gl_Position&lt;/code&gt;&lt;/strong&gt; is the output — where this vertex appears on screen. It's a &lt;code&gt;vec4(x, y, z, w)&lt;/code&gt; with four components:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;x, y&lt;/strong&gt; — the vertex position in clip space (provided by &lt;code&gt;positions[gl_VertexIndex]&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;z&lt;/strong&gt; (the &lt;code&gt;0.0&lt;/code&gt;) — the depth value. This determines how "near" or "far" the vertex is. It matters when triangles overlap — the GPU uses the depth buffer to decide which one is in front. For a flat 2D triangle, 0.0 (the middle of the depth range) works fine.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;w&lt;/strong&gt; (the &lt;code&gt;1.0&lt;/code&gt;) — the homogeneous coordinate, used for &lt;strong&gt;perspective division&lt;/strong&gt;. After the vertex shader runs, the GPU divides x, y, and z by w to get the final position. With &lt;code&gt;w = 1.0&lt;/code&gt;, the coordinates pass through unchanged. When we add a perspective camera later, the projection matrix will produce w values other than 1.0, which is what makes distant objects appear smaller.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;layout(location = 0) out vec3 fragColor&lt;/code&gt;&lt;/strong&gt; declares a variable that passes data from the vertex shader to the fragment shader. Let's break the syntax down:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layout(location = 0)&lt;/code&gt;&lt;/strong&gt; — assigns this variable to &lt;strong&gt;output slot 0&lt;/strong&gt;. The number is an index that connects outputs to inputs between shader stages. The fragment shader must declare a matching &lt;code&gt;layout(location = 0) in vec3 ...&lt;/code&gt; to receive this data. If you had a second output, you'd use &lt;code&gt;location = 1&lt;/code&gt;, and so on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;out&lt;/code&gt;&lt;/strong&gt; — marks this as an output variable (data flowing &lt;em&gt;out&lt;/em&gt; of the vertex shader to the next stage).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vec3 fragColor&lt;/code&gt;&lt;/strong&gt; — the type and name. A &lt;code&gt;vec3&lt;/code&gt; holds 3 floats, which we use for RGB color.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The GPU automatically &lt;strong&gt;interpolates&lt;/strong&gt; this value across the triangle's surface. So even though we set red, green, and blue at the three corners, every pixel in between gets a smooth gradient. This is how we get the rainbow effect without any extra work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is interpolation?&lt;/strong&gt; Interpolation means calculating values between known points. If one corner of the triangle is red and another is green, what color should a pixel halfway between them be? The GPU answers this automatically: 50% red + 50% green = yellow. It does this for every pixel across the triangle's surface, producing smooth gradients. This is a hardware feature — the GPU does it for free, with no extra code needed.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The fragment shader is simpler:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight glsl"&gt;&lt;code&gt;&lt;span class="cp"&gt;#version 450
&lt;/span&gt;
&lt;span class="k"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="kt"&gt;vec3&lt;/span&gt; &lt;span class="n"&gt;fragColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;out&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt; &lt;span class="n"&gt;outColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;outColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;vec4&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fragColor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the symmetry with the vertex shader:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layout(location = 0) in vec3 fragColor&lt;/code&gt;&lt;/strong&gt; — receives the interpolated color from the vertex shader. The &lt;code&gt;location = 0&lt;/code&gt; matches the &lt;code&gt;location = 0&lt;/code&gt; on the vertex shader's &lt;code&gt;out&lt;/code&gt; declaration — that's how the GPU knows to wire them together. The &lt;code&gt;in&lt;/code&gt; keyword (instead of &lt;code&gt;out&lt;/code&gt;) marks this as an input. The variable names don't technically need to match — only the &lt;code&gt;location&lt;/code&gt; number and type (&lt;code&gt;vec3&lt;/code&gt;) matter — but using the same name keeps it readable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;layout(location = 0) out vec4 outColor&lt;/code&gt;&lt;/strong&gt; — the fragment shader's own output. This writes to &lt;code&gt;pColorAttachments[0]&lt;/code&gt; in the render pass — the swapchain image. The &lt;code&gt;1.0&lt;/code&gt; appended in &lt;code&gt;vec4(fragColor, 1.0)&lt;/code&gt; is the alpha channel (fully opaque).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's it — the GPU's interpolation hardware did the hard work between the two shaders.&lt;/p&gt;

&lt;h3&gt;
  
  
  GLSL → SPIR-V compilation
&lt;/h3&gt;

&lt;p&gt;In WebGL, you pass shader source code as strings and the browser compiles them at runtime. Vulkan doesn't work that way. Shaders must be compiled ahead of time into &lt;strong&gt;SPIR-V&lt;/strong&gt; — a binary intermediate format, like bytecode.&lt;/p&gt;

&lt;p&gt;Think of it as the difference between JavaScript and WebAssembly. JavaScript is text that the engine parses and compiles at runtime. WebAssembly is pre-compiled bytecode that the engine can load and execute directly. SPIR-V is the WebAssembly of GPU shaders.&lt;/p&gt;

&lt;p&gt;The compiler is &lt;code&gt;glslc&lt;/code&gt; (part of the Vulkan SDK). Our CMake build handles this automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="nb"&gt;find_program&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;GLSLC glslc&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;GLOB_RECURSE SHADER_SOURCES
    src/engine/assets/shaders/*.vert
    src/engine/assets/shaders/*.frag
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;foreach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;SHADER &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER_SOURCES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;get_filename_component&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;SHADER_NAME &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; NAME&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;SPV &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER_OUTPUT_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.spv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;add_custom_command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        OUTPUT &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SPV&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        COMMAND &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;GLSLC&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; -o &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SPV&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        DEPENDS &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;SHADER&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;endforeach&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each &lt;code&gt;.vert&lt;/code&gt; and &lt;code&gt;.frag&lt;/code&gt; file gets compiled to a &lt;code&gt;.spv&lt;/code&gt; binary in the build directory. When the engine runs, it loads these &lt;code&gt;.spv&lt;/code&gt; files from disk.&lt;/p&gt;

&lt;h3&gt;
  
  
  Loading shader modules
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;Shader&lt;/code&gt; struct loads the compiled SPIR-V files and creates Vulkan &lt;strong&gt;shader modules&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Shader&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkShaderModule&lt;/span&gt; &lt;span class="n"&gt;vertModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;VkShaderModule&lt;/span&gt; &lt;span class="n"&gt;fragModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Shader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;vertCode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;fragCode&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;static&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;ReadFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;std::vector&amp;lt;char&amp;gt;&lt;/code&gt;&lt;/strong&gt; — if you're coming from JavaScript, a &lt;code&gt;vector&lt;/code&gt; is the C++ equivalent of an &lt;code&gt;Array&lt;/code&gt;. It's a dynamically-sized, contiguous block of memory. &lt;code&gt;std::vector&amp;lt;char&amp;gt;&lt;/code&gt; is an array of bytes — essentially a &lt;code&gt;Buffer&lt;/code&gt; or &lt;code&gt;Uint8Array&lt;/code&gt; in Node.js. We use it to hold the raw binary content of the SPIR-V files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;static&lt;/code&gt;&lt;/strong&gt; on the &lt;code&gt;ReadFile&lt;/code&gt; method means it belongs to the struct itself, not to any instance. It's like a &lt;code&gt;static&lt;/code&gt; method in a JavaScript class — you call it as &lt;code&gt;Shader::ReadFile(path)&lt;/code&gt; rather than on an instance. It doesn't need access to &lt;code&gt;vertModule&lt;/code&gt; or &lt;code&gt;fragModule&lt;/code&gt;, so it doesn't need an instance.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;ReadFile&lt;/code&gt; method does something specific to native development: it resolves the file path &lt;strong&gt;relative to the executable's location&lt;/strong&gt;, not the current working directory. In Node.js, &lt;code&gt;fs.readFileSync('./shaders/triangle.vert.spv')&lt;/code&gt; reads relative to where you ran &lt;code&gt;node&lt;/code&gt;. In our case, the shader files are placed next to the executable by CMake, so we use &lt;code&gt;_NSGetExecutablePath&lt;/code&gt; (macOS-specific) to find where the binary lives and build the path from there. This ensures the shaders are found regardless of which directory you run the demo from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;reinterpret_cast&amp;lt;const uint32_t*&amp;gt;(code.data())&lt;/code&gt;&lt;/strong&gt; — this appears in the shader module creation code and deserves explanation. The cast is needed because of a type mismatch between two APIs: C++'s file I/O reads data as &lt;code&gt;char*&lt;/code&gt; (bytes), but Vulkan expects SPIR-V data as &lt;code&gt;uint32_t*&lt;/code&gt; (pointer to 32-bit integers). Even though the &lt;code&gt;.spv&lt;/code&gt; file contains 32-bit SPIR-V words, &lt;code&gt;std::ifstream::read()&lt;/code&gt; has no concept of that — it treats all files as streams of bytes. So the data lands in a &lt;code&gt;std::vector&amp;lt;char&amp;gt;&lt;/code&gt;, and we need &lt;code&gt;reinterpret_cast&lt;/code&gt; to tell the compiler: "the bytes at this address — treat them as 32-bit integers instead." This doesn't change the data at all — it changes how the compiler &lt;em&gt;interprets&lt;/em&gt; the pointer type. It's like using a &lt;code&gt;DataView&lt;/code&gt; in JavaScript to read the same &lt;code&gt;ArrayBuffer&lt;/code&gt; as either bytes or 32-bit values. The cast is zero-cost at runtime — it generates no instructions and performs no conversion. It's purely a compile-time mechanism. This is unavoidable: as long as you read files through C++'s &lt;code&gt;char&lt;/code&gt;-based I/O and pass the data to an API that expects a different type, a cast is needed at one boundary or the other.&lt;/p&gt;

&lt;p&gt;A key detail: shader modules are only needed during pipeline creation. Once the pipeline is built, the GPU has the shader programs baked in, and the modules can be destroyed. That's why in the renderer, we call &lt;code&gt;shader.Destroy()&lt;/code&gt; right after creating the pipeline — the modules have served their purpose.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Graphics Pipeline
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/pipeline.h" rel="noopener noreferrer"&gt;Source: pipeline.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/pipeline.cpp" rel="noopener noreferrer"&gt;pipeline.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The graphics pipeline is the biggest, most detailed object in Vulkan. It describes &lt;strong&gt;everything&lt;/strong&gt; the GPU needs to know to turn vertices into pixels on screen. Where WebGL lets you change individual state bits on the fly (&lt;code&gt;gl.enable(gl.DEPTH_TEST)&lt;/code&gt;, &lt;code&gt;gl.blendFunc(...)&lt;/code&gt;), Vulkan makes you declare all of that upfront in a single immutable object.&lt;/p&gt;

&lt;p&gt;Think of it like this. In Express.js, you might configure middleware one piece at a time — add CORS here, add compression there, add auth somewhere else. Each piece of middleware can be swapped at runtime. A Vulkan pipeline is the opposite approach: imagine if you had to declare your entire Express middleware stack as one frozen object at startup and couldn't change it. That's the tradeoff Vulkan makes — less flexibility at runtime in exchange for zero overhead, because the GPU driver can optimize the entire pipeline as a single unit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Pipeline&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkPipelineLayout&lt;/span&gt; &lt;span class="n"&gt;layout&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;VkPipeline&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VkRenderPass&lt;/span&gt; &lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VkExtent2D&lt;/span&gt; &lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="n"&gt;VkShaderModule&lt;/span&gt; &lt;span class="n"&gt;vertModule&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VkShaderModule&lt;/span&gt; &lt;span class="n"&gt;fragModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creating a pipeline means configuring every stage of the GPU's rendering process. Let's walk through each one.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shader stages
&lt;/h3&gt;

&lt;p&gt;First, we tell the pipeline which shaders to use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineShaderStageCreateInfo&lt;/span&gt; &lt;span class="n"&gt;vertStage&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;vertStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_SHADER_STAGE_VERTEX_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;vertStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vertModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;vertStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;VkPipelineShaderStageCreateInfo&lt;/span&gt; &lt;span class="n"&gt;fragStage&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;fragStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_SHADER_STAGE_FRAGMENT_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fragStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fragModule&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fragStage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"main"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pName&lt;/code&gt; is the entry point function name in the shader — just like how a C++ program starts at &lt;code&gt;main()&lt;/code&gt;, the shader starts at whatever function you specify here. You could technically have multiple entry points in one shader module, but &lt;code&gt;"main"&lt;/code&gt; is the convention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vertex input
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineVertexInputStateCreateInfo&lt;/span&gt; &lt;span class="n"&gt;vertexInput&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;vertexInput&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_STRUCTURE_TYPE_PIPELINE_VERTEX_INPUT_STATE_CREATE_INFO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This describes the format of vertex data coming from the CPU. Since our triangle positions are hardcoded in the shader, we leave this empty — no vertex buffers, no attributes. In a real application, this is where you'd describe the layout of your mesh data: "each vertex has a 3-float position, a 3-float normal, a 2-float UV coordinate, at these byte offsets."&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a mesh?&lt;/strong&gt; A mesh is a collection of vertices, edges, and faces that defines the shape of a 3D object. Think of it as a wireframe model — a bunch of triangles stitched together to approximate a surface. A cube is 8 vertices and 12 triangles. A character model might be thousands of vertices and tens of thousands of triangles. The mesh data (positions, normals, colors, texture coordinates for each vertex) is what gets uploaded from the CPU to the GPU via vertex buffers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Input assembly
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineInputAssemblyStateCreateInfo&lt;/span&gt; &lt;span class="n"&gt;inputAssembly&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;inputAssembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;topology&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;inputAssembly&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primitiveRestartEnable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells the GPU how to interpret the vertices. &lt;code&gt;TRIANGLE_LIST&lt;/code&gt; means "every three consecutive vertices form one triangle." Other options include &lt;code&gt;LINE_LIST&lt;/code&gt; (draw lines), &lt;code&gt;POINT_LIST&lt;/code&gt; (draw dots), and &lt;code&gt;TRIANGLE_STRIP&lt;/code&gt; (each new vertex forms a triangle with the previous two). For most 3D rendering, you'll use &lt;code&gt;TRIANGLE_LIST&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Viewport and scissor
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkViewport&lt;/span&gt; &lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;minDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;viewport&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxDepth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;VkRect2D&lt;/span&gt; &lt;span class="n"&gt;scissor&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;scissor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;strong&gt;viewport&lt;/strong&gt; defines the transformation from clip space (the -1 to 1 range in the shader) to pixel coordinates on the framebuffer. If your window is 800×600, the viewport maps the normalized coordinates to those 800×600 pixels.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;scissor&lt;/strong&gt; is a clipping rectangle — pixels outside this rectangle are discarded. Usually it matches the viewport. Think of it as &lt;code&gt;overflow: hidden&lt;/code&gt; in CSS. The viewport says "scale the content to fit here," and the scissor says "cut off anything outside this box."&lt;/p&gt;

&lt;h3&gt;
  
  
  Rasterizer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineRasterizationStateCreateInfo&lt;/span&gt; &lt;span class="n"&gt;rasterizer&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;rasterizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;polygonMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_POLYGON_MODE_FILL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rasterizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cullMode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_CULL_MODE_BACK_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rasterizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;frontFace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_FRONT_FACE_CLOCKWISE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rasterizer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lineWidth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rasterizer converts the triangle (defined by three vertex positions) into a set of pixel-sized &lt;strong&gt;fragments&lt;/strong&gt;. This is the step where geometry becomes pixels.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is rasterization?&lt;/strong&gt; Rasterization is the process of converting vector shapes (triangles defined by vertex coordinates) into a grid of pixels. It's the same concept as when your browser renders an SVG — the browser takes the mathematical description of a shape and figures out which pixels on screen fall inside it. The GPU does this in hardware, for millions of triangles per frame.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a fragment?&lt;/strong&gt; A fragment is a candidate pixel — a pixel-sized sample generated by the rasterizer that &lt;em&gt;might&lt;/em&gt; end up on screen. It becomes an actual pixel after passing through the fragment shader and any blending/depth tests. The distinction matters because multiple fragments can compete for the same pixel position (e.g., overlapping triangles), and only one wins.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;polygonMode = FILL&lt;/code&gt;&lt;/strong&gt; — fill the triangle with color. Alternatives are &lt;code&gt;LINE&lt;/code&gt; (wireframe) and &lt;code&gt;POINT&lt;/code&gt; (just the corners).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cullMode = BACK_BIT&lt;/code&gt;&lt;/strong&gt; — don't render triangles facing away from the camera. If a triangle's vertices appear in clockwise order on screen, it's front-facing; counter-clockwise means it's facing away. This optimization skips roughly half the triangles in a 3D scene (the back sides of objects you can't see).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;frontFace = CLOCKWISE&lt;/code&gt;&lt;/strong&gt; — defines which vertex winding order means "front-facing."&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Multisampling
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineMultisampleStateCreateInfo&lt;/span&gt; &lt;span class="n"&gt;multisampling&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;multisampling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sampleShadingEnable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;multisampling&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rasterizationSamples&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_SAMPLE_COUNT_1_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multisampling is anti-aliasing at the hardware level — rendering each pixel at multiple sub-pixel positions and averaging the results. We disable it for now (&lt;code&gt;1_BIT&lt;/code&gt; = one sample per pixel = no multisampling). Think of it as the &lt;code&gt;image-rendering&lt;/code&gt; CSS property — it controls smoothing quality at the edges of shapes.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is anti-aliasing?&lt;/strong&gt; Anti-aliasing smooths out the jagged, staircase-like edges ("jaggies") that appear when diagonal or curved lines are drawn on a pixel grid. Without it, the edges of your triangle would look like tiny stairsteps. With it, the GPU blends edge pixels with the background to create the illusion of a smoother line. You've seen this in browsers — text and SVGs look smooth because the browser applies anti-aliasing automatically.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Color blending
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineColorBlendAttachmentState&lt;/span&gt; &lt;span class="n"&gt;colorBlendAttachment&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;colorBlendAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;colorWriteMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="n"&gt;VK_COLOR_COMPONENT_R_BIT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;VK_COLOR_COMPONENT_G_BIT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;VK_COLOR_COMPONENT_B_BIT&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;VK_COLOR_COMPONENT_A_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;colorBlendAttachment&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;blendEnable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_FALSE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Color blending controls what happens when a new pixel overlaps an existing one. With blending disabled, the new pixel simply replaces the old one. With blending enabled, you can do transparency effects (like &lt;code&gt;mix-blend-mode&lt;/code&gt; in CSS or &lt;code&gt;globalCompositeOperation&lt;/code&gt; in canvas). The &lt;code&gt;colorWriteMask&lt;/code&gt; says "write all four channels" — red, green, blue, and alpha. The &lt;code&gt;|&lt;/code&gt; operator combines the bit flags, a common C/C++ pattern for expressing combinations of options.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pipeline layout
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPipelineLayoutCreateInfo&lt;/span&gt; &lt;span class="n"&gt;layoutInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;layoutInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout describes what external data the pipeline receives — &lt;strong&gt;push constants&lt;/strong&gt; (small chunks of data pushed per draw call) and &lt;strong&gt;descriptor sets&lt;/strong&gt; (references to buffers, textures, etc.). We don't use either yet, so the layout is empty. Later, when we add textures and camera matrices, this is where we'll declare them.&lt;/p&gt;

&lt;h3&gt;
  
  
  Putting it together
&lt;/h3&gt;

&lt;p&gt;All these pieces feed into a single &lt;code&gt;VkGraphicsPipelineCreateInfo&lt;/code&gt; struct that creates the final pipeline object. This is one of the most expensive calls in Vulkan — the driver compiles and optimizes the entire pipeline state into GPU-specific instructions. That's why pipelines are created once and reused, never modified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Command Pool and Command Buffers
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/command_pool.h" rel="noopener noreferrer"&gt;Source: command_pool.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/command_pool.cpp" rel="noopener noreferrer"&gt;command_pool.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Vulkan, you don't call the GPU directly. You &lt;strong&gt;record&lt;/strong&gt; commands into a &lt;strong&gt;command buffer&lt;/strong&gt;, and then &lt;strong&gt;submit&lt;/strong&gt; the entire buffer to the GPU at once. Think of it like writing a batch script instead of typing commands one by one — you prepare all the instructions ahead of time, and the GPU executes them as a unit.&lt;/p&gt;

&lt;p&gt;If you've used Web Workers, the model is similar. You don't call functions on the worker directly — you post a message containing all the work, and the worker processes it asynchronously. Command buffers are the messages, and the GPU is the worker.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;CommandPool&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkCommandPool&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;vector&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;VkCommandBuffer&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;CommandPool&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;CommandPool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;queueFamily&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;bufferCount&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkDevice&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;command pool&lt;/strong&gt; is a memory allocator for command buffers. You create one pool per queue family, and it hands out command buffers from a pre-allocated memory region. This is more efficient than allocating each buffer individually — the same reason Node.js uses a Buffer pool internally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkCommandPoolCreateInfo&lt;/span&gt; &lt;span class="n"&gt;poolInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;poolInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_COMMAND_POOL_CREATE_RESET_COMMAND_BUFFER_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;poolInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;queueFamilyIndex&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queueFamily&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;RESET_COMMAND_BUFFER_BIT&lt;/code&gt; flag means we can reset and re-record individual command buffers. Without it, you'd have to reset the entire pool at once. We need per-buffer reset because we re-record commands every frame (the swapchain image index changes).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Command buffers&lt;/strong&gt; are allocated from the pool. We allocate one per swapchain image — while the GPU is executing commands for one image, we can record commands for the next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkCommandBufferAllocateInfo&lt;/span&gt; &lt;span class="n"&gt;allocInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;allocInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commandPool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;allocInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_COMMAND_BUFFER_LEVEL_PRIMARY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;allocInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commandBufferCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bufferCount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;PRIMARY&lt;/code&gt; command buffers are submitted directly to the GPU queue. (There are also &lt;code&gt;SECONDARY&lt;/code&gt; buffers that are called from primary buffers — like sub-functions, useful for multi-threaded recording. We don't need them yet.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Framebuffers
&lt;/h2&gt;

&lt;p&gt;Framebuffers don't have their own file — they're created in the renderer because they're essentially just glue between the render pass and the swapchain images.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;framebuffer&lt;/strong&gt; binds specific images to the attachment slots defined in the render pass. Remember, the render pass said "I'll write to one color attachment." The framebuffer says "and that attachment is &lt;em&gt;this specific&lt;/em&gt; swapchain image view."&lt;/p&gt;

&lt;p&gt;If the render pass is a form template with blank fields, the framebuffer is the filled-in form — it provides the actual images.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkFramebufferCreateInfo&lt;/span&gt; &lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;attachmentCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pAttachments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attachments&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// the swapchain image view&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swapchain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swapchain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;fbInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;layers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We create one framebuffer per swapchain image. If the swapchain has 3 images, we have 3 framebuffers. Each one points to its respective image view so that when we start a render pass, Vulkan knows exactly which image to render into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Synchronization
&lt;/h2&gt;

&lt;p&gt;This is the part of Vulkan that has no equivalent in web development. In JavaScript, the event loop handles all timing for you — you call &lt;code&gt;requestAnimationFrame&lt;/code&gt; and the browser tells you when to draw. In Vulkan, you manage all the timing yourself, and getting it wrong means frames tear, the GPU reads stale data, or your program crashes.&lt;/p&gt;

&lt;p&gt;We need three types of synchronization primitives:&lt;/p&gt;

&lt;h3&gt;
  
  
  Semaphores (GPU ↔ GPU)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Semaphores&lt;/strong&gt; synchronize operations within the GPU. They're signals that one GPU operation raises when it's done, and another waits for before starting. Think of them as &lt;code&gt;Promise&lt;/code&gt; objects — one operation resolves the promise, and another operation awaits it.&lt;/p&gt;

&lt;p&gt;We create two per frame slot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;imageAvailableSemaphore&lt;/code&gt;&lt;/strong&gt; — signaled when the swapchain image has been acquired and is ready to be rendered to&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;renderFinishedSemaphore&lt;/code&gt;&lt;/strong&gt; — signaled when rendering is complete and the image is ready to be presented on screen&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Fences (GPU → CPU)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Fences&lt;/strong&gt; synchronize the GPU with the CPU. They let the CPU wait until the GPU finishes a specific operation. If semaphores are GPU-to-GPU &lt;code&gt;Promises&lt;/code&gt;, fences are &lt;code&gt;await&lt;/code&gt; on the CPU side.&lt;/p&gt;

&lt;p&gt;We create one fence per frame slot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;inFlightFence&lt;/code&gt;&lt;/strong&gt; — the CPU waits on this before recording new commands, ensuring the GPU is done with the previous frame's commands for this slot
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkFenceCreateInfo&lt;/span&gt; &lt;span class="n"&gt;fenceInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;fenceInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_FENCE_CREATE_SIGNALED_BIT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;SIGNALED_BIT&lt;/code&gt; flag starts the fence in the "already done" state. Without this, the very first frame would wait forever — there's no previous frame to finish, so the fence would never be signaled. Starting it as signaled lets the first frame pass through.&lt;/p&gt;

&lt;h3&gt;
  
  
  Frames in flight
&lt;/h3&gt;

&lt;p&gt;We use &lt;strong&gt;two frames in flight&lt;/strong&gt; (&lt;code&gt;MAX_FRAMES_IN_FLIGHT = 2&lt;/code&gt;). This means the CPU can be recording commands for frame N+1 while the GPU is still executing frame N. It's double buffering for command submission — the same concept as the swapchain's image buffering, but applied to the CPU/GPU pipeline.&lt;/p&gt;

&lt;p&gt;Each frame slot has its own semaphores and fence, so they don't interfere with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Drawing a frame
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/renderer/renderer.cpp" rel="noopener noreferrer"&gt;Source: renderer.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is where everything comes together. Every single frame, the &lt;code&gt;DrawFrame&lt;/code&gt; method executes a sequence that touches every component we've built. Let's walk through it.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Wait for the previous frame
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;vkWaitForFences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inFlightFences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;VK_TRUE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UINT64_MAX&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;vkResetFences&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inFlightFences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before we do anything, we wait for the GPU to finish processing the last frame that used this slot. &lt;code&gt;UINT64_MAX&lt;/code&gt; means "wait forever" — we have no timeout, we just block until it's done. Then we reset the fence so it can be used again.&lt;/p&gt;

&lt;p&gt;This is the &lt;code&gt;await&lt;/code&gt; in our rendering loop. Without it, we'd pile up commands faster than the GPU can execute them.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Acquire a swapchain image
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt; &lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;vkAcquireNextImageKHR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;swapchain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;swapchain&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UINT64_MAX&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="n"&gt;imageAvailableSemaphores&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;VK_NULL_HANDLE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask the swapchain for the next available image to render to. The &lt;code&gt;imageAvailableSemaphore&lt;/code&gt; will be signaled when the image is ready. We get back an &lt;code&gt;imageIndex&lt;/code&gt; — which of the swapchain images we'll use this frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Record commands
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkCommandBuffer&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;commandPool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;vkResetCommandBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;VkCommandBufferBeginInfo&lt;/span&gt; &lt;span class="n"&gt;beginInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;beginInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sType&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;vkBeginCommandBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;beginInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We grab the command buffer for this image, reset it (clear the previous frame's commands), and start recording. Everything between &lt;code&gt;vkBeginCommandBuffer&lt;/code&gt; and &lt;code&gt;vkEndCommandBuffer&lt;/code&gt; is a batch of GPU instructions.&lt;/p&gt;

&lt;p&gt;The actual draw commands are recorded inside a render pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkClearValue&lt;/span&gt; &lt;span class="n"&gt;clearColor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{{{&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;}}};&lt;/span&gt;

&lt;span class="n"&gt;VkRenderPassBeginInfo&lt;/span&gt; &lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderPass&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderPass&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;framebuffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;framebuffers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;renderArea&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swapchain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extent&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clearValueCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pClearValues&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;clearColor&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;vkCmdBeginRenderPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;rpBegin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VK_SUBPASS_CONTENTS_INLINE&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;vkCmdBindPipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VK_PIPELINE_BIND_POINT_GRAPHICS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;vkCmdDraw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;vkCmdEndRenderPass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;vkEndCommandBuffer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The triple-brace &lt;code&gt;{{{0.0f, 0.0f, 0.0f, 1.0f}}}&lt;/code&gt; for the clear color looks unusual. This is because &lt;code&gt;VkClearValue&lt;/code&gt; is a &lt;strong&gt;union&lt;/strong&gt; — a C/C++ type that can hold different data in the same memory (a color, a depth value, or a stencil value). The outer braces initialize the union, the middle braces initialize its &lt;code&gt;color&lt;/code&gt; member, and the inner braces initialize the &lt;code&gt;float32[4]&lt;/code&gt; array inside the color. It's like a discriminated union in TypeScript (&lt;code&gt;type ClearValue = { color: [number, number, number, number] } | { depth: number }&lt;/code&gt;) except without any tag field — you just write to whichever variant you need.&lt;/p&gt;

&lt;p&gt;Three commands do all the work:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vkCmdBeginRenderPass&lt;/code&gt;&lt;/strong&gt; — start the render pass, targeting the framebuffer for this swapchain image. The clear color (black, fully opaque) is applied here because we set &lt;code&gt;loadOp = CLEAR&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vkCmdBindPipeline&lt;/code&gt;&lt;/strong&gt; — activate our graphics pipeline (which shaders to use, how to rasterize, etc.).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;vkCmdDraw(cmd, 3, 1, 0, 0)&lt;/code&gt;&lt;/strong&gt; — draw 3 vertices, 1 instance. This invokes the vertex shader 3 times (once per vertex), the rasterizer generates fragments for the triangle, and the fragment shader colors each pixel.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. Submit to the GPU
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkSubmitInfo&lt;/span&gt; &lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitSemaphoreCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pWaitSemaphores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waitSemaphores&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;           &lt;span class="c1"&gt;// wait for image to be available&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pWaitDstStageMask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;waitStages&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;             &lt;span class="c1"&gt;// wait at the color output stage&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;commandBufferCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pCommandBuffers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;signalSemaphoreCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pSignalSemaphores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signalSemaphores&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// signal when rendering is done&lt;/span&gt;

&lt;span class="n"&gt;vkQueueSubmit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;graphicsQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;submitInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inFlightFences&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We submit the recorded command buffer to the GPU's graphics queue. The submit info ties together the synchronization:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wait&lt;/strong&gt; on &lt;code&gt;imageAvailableSemaphore&lt;/code&gt; before writing color output&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal&lt;/strong&gt; &lt;code&gt;renderFinishedSemaphore&lt;/code&gt; when rendering completes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Signal&lt;/strong&gt; &lt;code&gt;inFlightFence&lt;/code&gt; so the CPU knows this frame's commands are done&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  5. Present
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;VkPresentInfoKHR&lt;/span&gt; &lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;{};&lt;/span&gt;
&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;waitSemaphoreCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pWaitSemaphores&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;signalSemaphores&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;    &lt;span class="c1"&gt;// wait for rendering to finish&lt;/span&gt;
&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;swapchainCount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pSwapchains&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;swapchains&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pImageIndices&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;imageIndex&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;vkQueuePresentKHR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;presentQueue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;presentInfo&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tell the presentation engine: "once the &lt;code&gt;renderFinishedSemaphore&lt;/code&gt; is signaled, display image &lt;code&gt;imageIndex&lt;/code&gt; on screen." The presentation engine handles the actual display timing (vsync, etc.).&lt;/p&gt;

&lt;h3&gt;
  
  
  6. Advance the frame counter
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;currentFrame&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;MAX_FRAMES_IN_FLIGHT&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cycle to the next frame slot. With &lt;code&gt;MAX_FRAMES_IN_FLIGHT = 2&lt;/code&gt;, this alternates between 0 and 1.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full frame in one picture
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;CPU                                    GPU
 │                                      │
 ├─ Wait for fence[N] ─────────────────►│ (GPU finishes previous frame N)
 ├─ Acquire image ─────────────────────►│ → signals imageAvailable[N]
 ├─ Record commands                     │
 ├─ Submit commands ───────────────────►│ waits imageAvailable[N]
 │                                      ├─ Execute render pass
 │                                      ├─ → signals renderFinished[N]
 │                                      ├─ → signals fence[N]
 ├─ Present ───────────────────────────►│ waits renderFinished[N]
 │                                      ├─ Display image
 ├─ N = (N + 1) % 2                     │
 └─ repeat                              │
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The updated application
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/2b4b2c3e1fc176efd9bca0f1da308c6897ade9fb/src/engine/core/application.cpp" rel="noopener noreferrer"&gt;Source: application.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The initialization sequence has grown to include all the new components:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitSwapchain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                           &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitPipeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitCommands&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitFramebuffers&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitSync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initialization order matters. Each step depends on what came before:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Depends on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Window&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Instance&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Surface&lt;/td&gt;
&lt;td&gt;Instance + Window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Instance + Surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Swapchain&lt;/td&gt;
&lt;td&gt;Device + Surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Render Pass&lt;/td&gt;
&lt;td&gt;Device + Swapchain format&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Shaders&lt;/td&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;Pipeline&lt;/td&gt;
&lt;td&gt;Device + Render Pass + Shaders + Swapchain extent&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;9&lt;/td&gt;
&lt;td&gt;Commands&lt;/td&gt;
&lt;td&gt;Device + Swapchain image count&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Framebuffers&lt;/td&gt;
&lt;td&gt;Device + Render Pass + Swapchain image views&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;Sync&lt;/td&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And the game loop now draws:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShouldClose&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DrawFrame&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line — &lt;code&gt;renderer.DrawFrame()&lt;/code&gt; — is all it took to go from an empty window to a rendered triangle. That's the value of the layered architecture we've been building. The complexity of synchronization, command recording, pipeline binding — none of it leaks into the application layer.&lt;/p&gt;

&lt;p&gt;Destruction now includes everything, in reverse order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Renderer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;vkDeviceWaitIdle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="c1"&gt;// semaphores, fences, framebuffers, command pool,&lt;/span&gt;
    &lt;span class="c1"&gt;// pipeline, render pass, swapchain, device, instance&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;vkDeviceWaitIdle&lt;/code&gt; call at the top is critical — it blocks until the GPU has finished all submitted work. Without it, we'd start destroying resources while the GPU is still using them, which would crash (or worse, corrupt memory silently).&lt;/p&gt;

&lt;h2&gt;
  
  
  The final result
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3x8kz6nuqtei0sb0qae.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3x8kz6nuqtei0sb0qae.png" alt=" " width="800" height="635"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A rainbow triangle on a black background — proof that every piece of the pipeline is working. The vertex shader placed three vertices, the rasterizer filled the triangle, the fragment shader colored each pixel using the GPU's hardware interpolation, and the presentation engine displayed the result on screen.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;We now have a working renderer. A triangle appears on screen, the colors are interpolated by the GPU, and the frame loop runs with proper synchronization. But the triangle is hardcoded in the shader — no vertex data comes from the CPU, no camera exists, and nothing moves.&lt;/p&gt;

&lt;p&gt;In the next part, we'll render a full-screen quad with a &lt;strong&gt;procedural checker texture&lt;/strong&gt; — generated entirely in the fragment shader, no image files needed. This will introduce UV coordinates, shader math, and the basics of pattern generation on the GPU.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cpp</category>
      <category>programming</category>
      <category>gamedev</category>
    </item>
    <item>
      <title>Building a 3D engine from scratch with C++ and Vulkan for web developers Part I: Bootstrapping Vulkan</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sun, 17 May 2026 11:54:30 +0000</pubDate>
      <link>https://forem.com/remojansen/building-a-3d-engine-from-scratch-with-c-and-vulkan-for-web-developers-part-i-bootstrapping-1bap</link>
      <guid>https://forem.com/remojansen/building-a-3d-engine-from-scratch-with-c-and-vulkan-for-web-developers-part-i-bootstrapping-1bap</guid>
      <description>&lt;h2&gt;
  
  
  Why this project exists
&lt;/h2&gt;

&lt;p&gt;My background is in web development. TypeScript, Node.js, React — that's been my world for years. I've spent a lot of time thinking about software architecture: SOLID principles, onion architecture, dependency injection, separation of concerns. All of that in the context of the web.&lt;/p&gt;

&lt;p&gt;But the reason I got into technology in the first place has nothing to do with web forms or REST APIs. It was Super Mario 64. Playing it as a kid was my first time experiencing 3D graphics, and it left a permanent mark. The name "Ultra" is a tribute to the Ultra 64 — the original name of the Nintendo 64 before it shipped.&lt;/p&gt;

&lt;p&gt;I've always wanted to understand how 3D engines work at every level. Not just "call a function and a triangle appears", but the actual machinery: how does the GPU know what to draw? How do pixels end up on the screen? What happens between your code and the display?&lt;/p&gt;

&lt;p&gt;This project is my attempt to answer those questions by building a 3D engine from scratch with C++ and Vulkan. And since I'm learning as I go, I'm documenting everything so it can help others who are on the same path — especially those coming from a web development background like me.&lt;/p&gt;

&lt;p&gt;The source code is hosted on &lt;a href="https://codeberg.org/" rel="noopener noreferrer"&gt;Codeberg&lt;/a&gt; — a non-profit alternative to GitHub, hosted in the EU. You can find the repository at &lt;a href="https://codeberg.org/remojansen/ultra" rel="noopener noreferrer"&gt;codeberg.org/remojansen/ultra&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;I'm not sure how far this project will go. I'm doing it for fun and education, so I'll keep at it for as long as I find it engaging and exciting. There's no roadmap, no deadline, no promise of a "complete" engine at the end — just curiosity and momentum.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This series assumes you're an experienced web developer. You should be comfortable with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;JavaScript and TypeScript&lt;/strong&gt; — you write JS/TS daily and understand its type system, module system, and async patterns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node.js&lt;/strong&gt; — you've built backend services, used &lt;code&gt;npm&lt;/code&gt;, and understand how the Node runtime works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Browser APIs&lt;/strong&gt; — you have a working knowledge of the DOM, &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, and how the browser renders pages.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Software architecture&lt;/strong&gt; — concepts like separation of concerns, dependency injection, and layered architectures aren't new to you.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No prior C++ or graphics programming experience is required — that's what this series teaches. But you should be the kind of developer who's comfortable reading documentation and figuring out new tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  The toolchain
&lt;/h2&gt;

&lt;p&gt;Before we write any engine code, we need to understand the tools. If you're coming from the JavaScript ecosystem, the C++ toolchain will feel different — there's no &lt;code&gt;npm install&lt;/code&gt; and no &lt;code&gt;node index.js&lt;/code&gt;. But the concepts map surprisingly well.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clang (the compiler)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;a href="https://clang.llvm.org/get_started.html" rel="noopener noreferrer"&gt;Getting Started with Clang&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In JavaScript, your code runs directly in V8 or SpiderMonkey. These engines use &lt;strong&gt;JIT (Just-In-Time) compilation&lt;/strong&gt; — they compile your code to machine code &lt;em&gt;while the program is running&lt;/em&gt;, optimizing hot paths on the fly based on actual usage patterns.&lt;/p&gt;

&lt;p&gt;C++ works the opposite way. Your code must be &lt;strong&gt;compiled&lt;/strong&gt; into a binary &lt;em&gt;before&lt;/em&gt; it can run. This is called &lt;strong&gt;AOT (Ahead-Of-Time) compilation&lt;/strong&gt;. Clang is the compiler we use — it reads your &lt;code&gt;.cpp&lt;/code&gt; files and produces machine code that runs directly on your CPU, with no runtime or interpreter involved.&lt;/p&gt;

&lt;p&gt;The tradeoff is straightforward: JIT compilation gives you fast startup and runtime adaptability (the engine can optimize code paths it sees running frequently), but there's overhead — the compiler runs alongside your program. AOT compilation is slow upfront (you have to compile before you can test), but the output is fully optimized native code with zero runtime overhead. For a real-time graphics engine where every microsecond matters, that's the tradeoff we want.&lt;/p&gt;

&lt;h3&gt;
  
  
  CMake (the build system generator)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;a href="https://cmake.org/download/" rel="noopener noreferrer"&gt;CMake Download&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Node.js, you have &lt;code&gt;package.json&lt;/code&gt; to describe your project. In C++, you have &lt;code&gt;CMakeLists.txt&lt;/code&gt;. CMake reads this file and generates the actual build instructions for your platform.&lt;/p&gt;

&lt;p&gt;It doesn't build your code directly — it generates build files for another tool (in our case, Ninja). This might feel like an unnecessary layer of indirection, but it's what allows the same project to build on macOS, Linux, and Windows without changes.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;CMakeLists.txt&lt;/code&gt; defines two targets:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cmake"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ultra engine library&lt;/span&gt;
&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;GLOB_RECURSE ULTRA_SOURCES src/engine/*.cpp&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;add_library&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;ultra_engine STATIC &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;ULTRA_SOURCES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_include_directories&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;ultra_engine PUBLIC &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;CMAKE_SOURCE_DIR&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;/src&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_link_libraries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;ultra_engine PUBLIC glfw Vulkan::Vulkan&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Demo application&lt;/span&gt;
&lt;span class="nb"&gt;file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;GLOB_RECURSE DEMO_SOURCES src/demo/*.cpp&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;add_executable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;demo &lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DEMO_SOURCES&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;target_link_libraries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;demo PRIVATE ultra_engine&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The engine is built as a &lt;strong&gt;static library&lt;/strong&gt; (&lt;code&gt;ultra_engine&lt;/code&gt;), and the demo game is built as an &lt;strong&gt;executable&lt;/strong&gt; (&lt;code&gt;demo&lt;/code&gt;) that links against it. Let's unpack what that means.&lt;/p&gt;

&lt;p&gt;An &lt;strong&gt;executable&lt;/strong&gt; is a binary that your OS can run directly — it has an entry point (&lt;code&gt;main()&lt;/code&gt;), and double-clicking it (or running it from a terminal) starts a process. A &lt;strong&gt;static library&lt;/strong&gt; is &lt;em&gt;not&lt;/em&gt; something you can run. It's a bundle of compiled code that sits there waiting to be included in an executable. Think of it as a &lt;code&gt;.jar&lt;/code&gt; file in Java or a compiled npm package — it contains useful code, but it needs a host program to actually execute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Linking&lt;/strong&gt; is the process that connects them. When the compiler builds &lt;code&gt;demo&lt;/code&gt;, it sees calls to functions like &lt;code&gt;Application()&lt;/code&gt; and &lt;code&gt;Run()&lt;/code&gt;. Those functions aren't defined in &lt;code&gt;demo&lt;/code&gt;'s own source code — they live in &lt;code&gt;ultra_engine&lt;/code&gt;. The linker's job is to resolve these references: it looks through the static library, finds the compiled code for each function, and copies it directly into the final executable. The result is a single binary (&lt;code&gt;demo&lt;/code&gt;) that contains everything it needs to run — both its own code and all the engine code baked in.&lt;/p&gt;

&lt;p&gt;This is fundamentally different from how JavaScript modules work. When you &lt;code&gt;import&lt;/code&gt; a package in Node.js, that package is loaded at runtime from &lt;code&gt;node_modules&lt;/code&gt;. With static linking, there is no runtime lookup — the library's code is physically embedded in the executable at build time. The static library file itself isn't needed after compilation; the executable is entirely self-contained.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ninja (the build executor)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;a href="https://ninja-build.org/" rel="noopener noreferrer"&gt;Ninja Getting Started&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ninja is the tool that actually runs the compiler. CMake generates the instructions, Ninja executes them. It's fast and minimal. You'll rarely interact with it directly — you just run &lt;code&gt;cmake --build build&lt;/code&gt; and CMake calls Ninja for you.&lt;/p&gt;

&lt;h3&gt;
  
  
  vcpkg (the package manager)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; &lt;a href="https://learn.microsoft.com/en-us/vcpkg/get_started/get-started" rel="noopener noreferrer"&gt;vcpkg Getting Started&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one will feel familiar. vcpkg is the closest thing C++ has to npm. We use it to install third-party libraries like GLFW. Dependencies are declared in &lt;code&gt;vcpkg.json&lt;/code&gt;, and vcpkg resolves, downloads, and builds them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"glfw3"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  clang-format and clang-tidy (code quality)
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Install:&lt;/strong&gt; Both ship with the &lt;a href="https://releases.llvm.org/download.html" rel="noopener noreferrer"&gt;LLVM toolchain&lt;/a&gt;. If you installed Clang, you likely already have them.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;clang-format&lt;/strong&gt; is Prettier for C++. It enforces consistent code style automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clang-tidy&lt;/strong&gt; is ESLint for C++. It performs static analysis and catches common bugs and anti-patterns at compile time.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;.h&lt;/code&gt; and &lt;code&gt;.cpp&lt;/code&gt; files
&lt;/h2&gt;

&lt;p&gt;In web development, you write everything in a single &lt;code&gt;.ts&lt;/code&gt; or &lt;code&gt;.js&lt;/code&gt; file. In C++, code is split into two file types:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Header files (&lt;code&gt;.h&lt;/code&gt;)&lt;/strong&gt; — These are the &lt;strong&gt;declarations&lt;/strong&gt;. They describe &lt;em&gt;what&lt;/em&gt; exists: the struct names, the method signatures, the types. Think of them as TypeScript interface files or &lt;code&gt;.d.ts&lt;/code&gt; declarations. Other files include headers to know what's available.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Source files (&lt;code&gt;.cpp&lt;/code&gt;)&lt;/strong&gt; — These are the &lt;strong&gt;implementations&lt;/strong&gt;. They contain the actual code that runs. Think of them as the concrete classes that implement a TypeScript interface.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This separation exists because the C++ compiler processes files independently. When &lt;code&gt;application.cpp&lt;/code&gt; needs to use the &lt;code&gt;Window&lt;/code&gt; struct, it doesn't read &lt;code&gt;window.cpp&lt;/code&gt; — it reads &lt;code&gt;window.h&lt;/code&gt; to learn the shape of &lt;code&gt;Window&lt;/code&gt;, and the linker connects everything at the end.&lt;/p&gt;

&lt;h2&gt;
  
  
  The directory architecture
&lt;/h2&gt;

&lt;p&gt;Separation of concerns matters just as much in a 3D engine as it does in a web application. We organize the source code into layers with clear responsibilities:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── engine/                  ← the engine (static library)
│   ├── core/
│   │   └── application     ← owns the game loop, orchestrates everything
│   ├── platform/
│   │   ├── window          ← OS window management (GLFW)
│   │   └── surface         ← bridge between the window and Vulkan
│   └── renderer/
│       ├── instance         ← Vulkan runtime initialization
│       ├── device           ← GPU selection and logical device
│       └── swapchain        ← image buffers for presenting frames
├── demo/
│   └── main.cpp            ← demo application that uses the engine
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dependency flow is one-directional:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;main.cpp → Application → Renderer
                        → Platform (Window, Surface)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;platform&lt;/code&gt; layer deals with anything OS-specific (creating a window, connecting it to Vulkan). The &lt;code&gt;renderer&lt;/code&gt; layer is pure Vulkan. The &lt;code&gt;core&lt;/code&gt; layer ties them together. The demo application only knows about &lt;code&gt;Application&lt;/code&gt; — it has no idea that GLFW or Vulkan exist.&lt;/p&gt;

&lt;p&gt;If you've worked with the onion architecture in Node.js, this will feel familiar. The inner layers don't know about the outer layers. The renderer doesn't know about the window. The application sits at the boundary and wires everything together, just like a composition root in a dependency injection setup.&lt;/p&gt;

&lt;h2&gt;
  
  
  The initialization flow
&lt;/h2&gt;

&lt;p&gt;Vulkan is an explicit API. Unlike OpenGL (or WebGL, if you've used it), Vulkan doesn't do anything for you behind the scenes. You have to set up every piece of the pipeline yourself. The initialization flow looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Instance → Window → Surface → Device → Swapchain
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step depends on the one before it. But before we dive in, let's make sure we understand the two main technologies we're working with.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: What are Vulkan and GLFW?
&lt;/h2&gt;

&lt;p&gt;If you're coming from web development, you've never had to think about how pixels get on the screen. The browser handles all of that. You write HTML and CSS, or draw to a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt;, and the browser's rendering engine figures out how to talk to the GPU.&lt;/p&gt;

&lt;p&gt;In native development, there is no browser. Your application talks to the GPU directly through a &lt;strong&gt;graphics API&lt;/strong&gt;. That's what &lt;strong&gt;Vulkan&lt;/strong&gt; is — a low-level API that lets you send commands to the GPU: "create an image buffer", "run this shader program", "draw these triangles", "present this frame to the screen." It's the equivalent of the WebGL API you might have seen in the browser, but much more explicit and verbose. Where WebGL hides most of the complexity, Vulkan exposes it all. You control memory allocation, synchronization, command recording — everything. That's what makes it powerful for high-performance engines, and also what makes it hard to learn.&lt;/p&gt;

&lt;p&gt;Vulkan is cross-platform — it runs on Windows, Linux, and Android natively. On macOS, it runs through &lt;strong&gt;MoltenVK&lt;/strong&gt;, a translation layer that converts Vulkan calls into Apple's Metal API under the hood. This means we write Vulkan code and it works on macOS, but there are a few extra setup steps (portability extensions) that we'll see shortly.&lt;/p&gt;

&lt;p&gt;One thing Vulkan does &lt;em&gt;not&lt;/em&gt; do is create windows. It's a graphics API, not a windowing API. It can render pixels, but it has no idea how to open a window on your operating system, handle keyboard input, or respond to a close button. For that, we need a separate library.&lt;/p&gt;

&lt;p&gt;That's where &lt;strong&gt;GLFW&lt;/strong&gt; comes in. GLFW is a small C library that handles the OS-level stuff: creating a window, processing input events (keyboard, mouse, gamepad), and providing the bridge between the OS window and whatever graphics API you're using. Think of it as the &lt;code&gt;window&lt;/code&gt; object in a browser — it gives you the container that your rendering will appear in, and it fires events when the user interacts with it.&lt;/p&gt;

&lt;p&gt;GLFW also knows about Vulkan specifically. It can tell you which &lt;strong&gt;Vulkan extensions&lt;/strong&gt; are needed on your platform to display rendered output in a window. Extensions in Vulkan are optional features — the core API is minimal, and everything platform-specific (like "how do I display pixels in a macOS window?") is provided as an extension. GLFW queries the system and returns the list of extensions you need to enable. We'll see this in action when we create the Vulkan instance.&lt;/p&gt;

&lt;p&gt;With that context, let's walk through each initialization step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: VkInstance
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/instance.h" rel="noopener noreferrer"&gt;Source: instance.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/instance.cpp" rel="noopener noreferrer"&gt;instance.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;VkInstance&lt;/code&gt; is the entry point to the Vulkan API. Creating it initializes the Vulkan runtime on your machine. Nothing Vulkan-related can happen without it.&lt;/p&gt;

&lt;p&gt;If you're coming from web development, think of it as opening a browser. The browser itself doesn't show any web page yet, but you now have access to the rendering engine. That's what &lt;code&gt;VkInstance&lt;/code&gt; is — you're telling the system "I want to use Vulkan."&lt;/p&gt;

&lt;p&gt;Let's look at the header:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;vulkan/vulkan.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Instance&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkInstance&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Instance&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things here that deserve explanation if you're new to C++:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;struct&lt;/code&gt;&lt;/strong&gt; — if you're coming from TypeScript, you'd expect to see &lt;code&gt;class&lt;/code&gt; here. C++ has both &lt;code&gt;struct&lt;/code&gt; and &lt;code&gt;class&lt;/code&gt;, and they're almost identical. The only difference is default visibility: in a &lt;code&gt;struct&lt;/code&gt;, everything is &lt;code&gt;public&lt;/code&gt; by default; in a &lt;code&gt;class&lt;/code&gt;, everything is &lt;code&gt;private&lt;/code&gt; by default. We use &lt;code&gt;struct&lt;/code&gt; throughout the engine because all our members are public — there's no reason to write &lt;code&gt;class&lt;/code&gt; and then immediately add &lt;code&gt;public:&lt;/code&gt; to undo the default. You'll see both conventions in C++ codebases; it's a style choice, not a functional one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#pragma once&lt;/code&gt;&lt;/strong&gt; is a preprocessor directive that tells the compiler "only include this file once, even if multiple files try to include it." Without it, you'd get duplicate definition errors. It's the C++ equivalent of making sure you don't import the same module twice — except in C++ the compiler doesn't handle it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;#include &amp;lt;vulkan/vulkan.h&amp;gt;&lt;/code&gt;&lt;/strong&gt; pulls in the Vulkan API declarations. This is conceptually the same as &lt;code&gt;import vulkan from 'vulkan'&lt;/code&gt; in JavaScript — it tells the compiler that types like &lt;code&gt;VkInstance&lt;/code&gt; exist and what they look like.&lt;/p&gt;

&lt;p&gt;When we create the instance, three things happen:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We describe our application&lt;/strong&gt; with &lt;code&gt;VkApplicationInfo&lt;/code&gt; — metadata like the app name and Vulkan API version. Think of it as a &lt;code&gt;User-Agent&lt;/code&gt; header in HTTP.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We ask GLFW for the required extensions&lt;/strong&gt; — as we discussed in Step 1, GLFW knows which Vulkan extensions are needed to present to a window on your platform. On macOS, this includes the MoltenVK portability extensions we mentioned earlier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;We enable validation layers&lt;/strong&gt; (in debug builds only) — these are a runtime linter. They watch every Vulkan API call and warn you if you're doing something wrong. Incredibly useful for learning.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Window
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/platform/window.h" rel="noopener noreferrer"&gt;Source: window.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/platform/window.cpp" rel="noopener noreferrer"&gt;window.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The window is your connection to the operating system. It's the rectangle on your screen where pixels will appear. We use GLFW to create and manage it — as we covered in Step 1, GLFW handles window creation, input, and OS events across platforms.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;GLFW/glfw3.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstdio&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;cstdlib&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Window&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;GLFWwindow&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="kt"&gt;bool&lt;/span&gt; &lt;span class="n"&gt;ShouldClose&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;PollEvents&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the &lt;code&gt;const&lt;/code&gt; keyword after the method declarations — &lt;code&gt;bool ShouldClose() const&lt;/code&gt;. This has no equivalent in JavaScript or TypeScript. It's a promise to the compiler that this method &lt;strong&gt;will not modify the object&lt;/strong&gt;. It only reads data, never writes it. If you accidentally try to change a member variable inside a &lt;code&gt;const&lt;/code&gt; method, the compiler will reject it. Think of it as a read-only contract — like &lt;code&gt;Readonly&amp;lt;T&amp;gt;&lt;/code&gt; in TypeScript, but enforced at the method level rather than on the type. &lt;code&gt;Destroy()&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; const because it modifies state (it tears down the window).&lt;/p&gt;

&lt;p&gt;This is the first time we see a &lt;strong&gt;pointer&lt;/strong&gt;, so let's talk about what &lt;code&gt;GLFWwindow* window&lt;/code&gt; means.&lt;/p&gt;

&lt;p&gt;In JavaScript, when you write &lt;code&gt;const element = document.getElementById('app')&lt;/code&gt;, you get a reference to a DOM element. You don't get the element itself — you get something that &lt;em&gt;points to&lt;/em&gt; where the element lives in memory. If the browser moved it in memory, your reference would be updated.&lt;/p&gt;

&lt;p&gt;In C++, a &lt;strong&gt;pointer&lt;/strong&gt; is the explicit version of this. &lt;code&gt;GLFWwindow*&lt;/code&gt; means "a memory address where a &lt;code&gt;GLFWwindow&lt;/code&gt; lives." The &lt;code&gt;*&lt;/code&gt; is what makes it a pointer. You don't own the GLFW window data directly — GLFW allocates it internally and gives you a pointer to it. When you pass this pointer to other GLFW functions, they follow the address to find the actual window data.&lt;/p&gt;

&lt;p&gt;The important thing to know right now is: a pointer is an address. &lt;code&gt;GLFWwindow* window&lt;/code&gt; means "window is a variable that holds the address of a &lt;code&gt;GLFWwindow&lt;/code&gt; somewhere in memory."&lt;/p&gt;

&lt;p&gt;The window has a simple job. It creates an OS window via GLFW and exposes three operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ShouldClose()&lt;/code&gt;&lt;/strong&gt; — has the user clicked the close button?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;PollEvents()&lt;/code&gt;&lt;/strong&gt; — check for OS events (mouse, keyboard, resize, close). Without this, the OS thinks your app is frozen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;Destroy()&lt;/code&gt;&lt;/strong&gt; — tear it all down.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note the &lt;code&gt;GLFW_NO_API&lt;/code&gt; hint in the implementation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;glfwWindowHint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;GLFW_CLIENT_API&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GLFW_NO_API&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells GLFW "don't set up OpenGL — we're going to use Vulkan instead." By default GLFW creates an OpenGL context, which we don't want.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: VkSurfaceKHR
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/platform/surface.h" rel="noopener noreferrer"&gt;Source: surface.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/platform/surface.cpp" rel="noopener noreferrer"&gt;surface.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The surface is the bridge between your OS window and Vulkan. It answers the question: "where should Vulkan draw its pixels?"&lt;/p&gt;

&lt;p&gt;In web terms, imagine you have a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element and a WebGL context. The canvas is the window. The WebGL context is Vulkan. The surface is the binding between them — it's what allows the rendering API to output to that specific rectangle on the screen.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="cp"&gt;#pragma once
&lt;/span&gt;
&lt;span class="cp"&gt;#define GLFW_INCLUDE_VULKAN
#include&lt;/span&gt; &lt;span class="cpf"&gt;&amp;lt;GLFW/glfw3.h&amp;gt;&lt;/span&gt;&lt;span class="cp"&gt;
&lt;/span&gt;
&lt;span class="k"&gt;struct&lt;/span&gt; &lt;span class="nc"&gt;Surface&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;VkSurfaceKHR&lt;/span&gt; &lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="n"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="n"&gt;Surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkInstance&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;GLFWwindow&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VkInstance&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;#define GLFW_INCLUDE_VULKAN&lt;/code&gt;&lt;/strong&gt; is a preprocessor macro. JavaScript has nothing like this. Before the compiler even sees your code, a separate step called the &lt;strong&gt;preprocessor&lt;/strong&gt; runs through it and performs text substitution. &lt;code&gt;#define GLFW_INCLUDE_VULKAN&lt;/code&gt; creates a flag — it doesn't produce any code itself. When GLFW's header file is included on the next line, it checks whether this flag exists, and if so, it also pulls in the Vulkan type declarations (like &lt;code&gt;VkInstance&lt;/code&gt; and &lt;code&gt;VkSurfaceKHR&lt;/code&gt;). Without it, GLFW wouldn't know about Vulkan types. Think of it as a compile-time feature flag — like an environment variable, but resolved before compilation rather than at runtime.&lt;/p&gt;

&lt;p&gt;The surface needs both the &lt;code&gt;VkInstance&lt;/code&gt; and the &lt;code&gt;GLFWwindow*&lt;/code&gt; because it sits between the two worlds. GLFW provides a helper function (&lt;code&gt;glfwCreateWindowSurface&lt;/code&gt;) that handles the platform-specific details — on macOS it creates a Metal surface, on Linux it creates an X11 or Wayland surface, on Windows it creates a Win32 surface.&lt;/p&gt;

&lt;p&gt;This is why the surface is created &lt;strong&gt;after&lt;/strong&gt; both the instance and the window exist, and why it lives in the &lt;code&gt;platform/&lt;/code&gt; directory — it fundamentally wraps an OS-level concept, even though it uses a Vulkan type.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: VkDevice
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/device.h" rel="noopener noreferrer"&gt;Source: device.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/device.cpp" rel="noopener noreferrer"&gt;device.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The device step is actually two things: picking a physical GPU and creating a logical device.&lt;/p&gt;

&lt;h3&gt;
  
  
  Picking a physical device
&lt;/h3&gt;

&lt;p&gt;Your machine might have multiple GPUs (an integrated one and a discrete one, for example). We need to pick one that can do what we need. The selection process works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Enumerate all GPUs on the system&lt;/li&gt;
&lt;li&gt;For each GPU, check its &lt;strong&gt;queue families&lt;/strong&gt; — groups of "workers" that can do different things (graphics, compute, transfer, presentation)&lt;/li&gt;
&lt;li&gt;Find a GPU that has both a &lt;strong&gt;graphics queue family&lt;/strong&gt; (can draw things) and a &lt;strong&gt;present queue family&lt;/strong&gt; (can display things on our surface)&lt;/li&gt;
&lt;li&gt;Check that the GPU supports the &lt;strong&gt;swapchain extension&lt;/strong&gt; (needed to present rendered frames)&lt;/li&gt;
&lt;li&gt;Pick the first GPU that passes all checks&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you're coming from web development, think of queue families as different thread pools. One pool handles graphics work, another handles displaying results on screen. Often they're the same pool, but the API makes you check.&lt;/p&gt;

&lt;h3&gt;
  
  
  Creating the logical device
&lt;/h3&gt;

&lt;p&gt;Once we've picked a GPU, we create a &lt;strong&gt;logical device&lt;/strong&gt; — our application's handle to the GPU. The distinction matters: &lt;code&gt;VkPhysicalDevice&lt;/code&gt; represents the actual hardware, &lt;code&gt;VkDevice&lt;/code&gt; represents our connection to it. Multiple applications can each have their own &lt;code&gt;VkDevice&lt;/code&gt; pointing at the same &lt;code&gt;VkPhysicalDevice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;When creating the logical device, we request:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Queues&lt;/strong&gt; from the families we identified — these are the mailboxes where we'll send draw commands&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Extensions&lt;/strong&gt; — specifically &lt;code&gt;VK_KHR_swapchain&lt;/code&gt;, so we can present frames&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creation, we retrieve the queue handles. These are what we'll use later to submit rendering commands and present images to the screen.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is a shader?&lt;/strong&gt; A shader is a small program that runs on the GPU. We'll write these in Part II. For now, just know that the GPU executes shader programs to determine where vertices appear and what color each pixel should be.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 6: Swapchain
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/swapchain.h" rel="noopener noreferrer"&gt;Source: swapchain.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/renderer/swapchain.cpp" rel="noopener noreferrer"&gt;swapchain.cpp&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is a frame?&lt;/strong&gt; A frame is a single complete image displayed on your screen. Movies run at 24 frames per second — 24 still images flashing by so fast they look like motion. Games work the same way, typically at 60 frames per second. Each frame is drawn from scratch by the GPU, displayed briefly, then replaced by the next one.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The swapchain is a queue of images that take turns being displayed on screen. Think of it like double or triple buffering in a &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; game.&lt;/p&gt;

&lt;p&gt;Imagine you have 2–3 offscreen canvases. You draw to one while the browser displays another. When you finish drawing, you swap them — the freshly drawn one goes to the screen, and the previously displayed one is freed up for the next frame. That's exactly what a swapchain does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Image A: [being displayed on screen]
Image B: [GPU is drawing the next frame here]
Image C: [waiting, ready for the GPU to use next]
         ↓
         swap → Image B goes to screen, Image A is now free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, the user would see half-finished frames — a visual glitch called &lt;strong&gt;tearing&lt;/strong&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is tearing?&lt;/strong&gt; Tearing is when the top half of the screen shows one frame and the bottom half shows the next, because the display refreshed mid-draw. It happens when the GPU writes to the same image the monitor is currently reading from. The swapchain prevents this by keeping displayed and in-progress images separate.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Creating the swapchain involves querying the surface for what it supports and choosing the best options:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Format&lt;/strong&gt; — the pixel format and color space. We prefer &lt;code&gt;B8G8R8A8_SRGB&lt;/code&gt; (BGRA 8-bit with sRGB). This is the standard format for monitors — sRGB ensures colors look correct.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Present mode&lt;/strong&gt; — how swapping works. &lt;code&gt;MAILBOX&lt;/code&gt; is triple buffering (low latency, GPU replaces queued frames). &lt;code&gt;FIFO&lt;/code&gt; is vsync (like &lt;code&gt;requestAnimationFrame&lt;/code&gt; — guaranteed to be available, smooth but higher latency). We prefer mailbox, fall back to FIFO.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;What is vsync?&lt;/strong&gt; Vsync (vertical sync) locks your frame rate to the monitor's refresh rate (usually 60Hz). It prevents tearing by waiting for the monitor to finish displaying one frame before swapping in the next. &lt;code&gt;requestAnimationFrame&lt;/code&gt; in browsers is essentially vsync — the browser calls your callback once per display refresh.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extent&lt;/strong&gt; — the resolution, usually matching the window size.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After creating the swapchain, we get the image handles (the driver created them, we just get references) and create &lt;strong&gt;image views&lt;/strong&gt; for each one. An image view is a "lens" that tells Vulkan how to interpret an image — its format, that it's 2D, that we care about the color channels.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it all together: Application
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/core/application.h" rel="noopener noreferrer"&gt;Source: application.h&lt;/a&gt; · &lt;a href="https://codeberg.org/remojansen/ultra/src/commit/d69088417e7cd90fc97921e2d07ba3c96f557130/src/engine/core/application.cpp" rel="noopener noreferrer"&gt;application.cpp&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The &lt;code&gt;Application&lt;/code&gt; struct sits at the top and orchestrates everything. It's the composition root — the one place that knows about all the pieces and wires them together.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;const&lt;/span&gt; &lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
      &lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitDevice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;InitSwapchain&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                           &lt;span class="k"&gt;static_cast&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;uint32_t&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two pieces of syntax here that have no JavaScript equivalent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Member initializer list&lt;/strong&gt; — the &lt;code&gt;: window(width, height, title), renderer(), surface(...)&lt;/code&gt; part after the constructor signature. In JavaScript, you'd write &lt;code&gt;this.window = new Window(width, height, title)&lt;/code&gt; inside the constructor body. C++ has a separate syntax for this because of how memory works: when an &lt;code&gt;Application&lt;/code&gt; is created, all its member variables need to be constructed &lt;em&gt;before&lt;/em&gt; the constructor body runs. The initializer list is where you tell the compiler &lt;em&gt;how&lt;/em&gt; to construct each member. If you skipped it and assigned inside the body instead, each member would first be default-constructed (possibly doing wasted work) and then reassigned — the initializer list avoids that double initialization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;static_cast&amp;lt;uint32_t&amp;gt;(width)&lt;/code&gt;&lt;/strong&gt; — explicit type conversion. In JavaScript, numbers are just numbers — there's one &lt;code&gt;number&lt;/code&gt; type. In C++, &lt;code&gt;int&lt;/code&gt; and &lt;code&gt;uint32_t&lt;/code&gt; (unsigned 32-bit integer) are different types, and the compiler will warn you about implicit conversions between them because going from signed to unsigned can lose information (negative values wrap around). &lt;code&gt;static_cast&lt;/code&gt; says "I know these types are different, and I'm intentionally converting." It's the C++ equivalent of writing &lt;code&gt;width as number&lt;/code&gt; in TypeScript — an explicit annotation that makes the intent clear.&lt;/p&gt;

&lt;p&gt;Creation order matters because each step depends on the previous one:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Order&lt;/th&gt;
&lt;th&gt;Component&lt;/th&gt;
&lt;th&gt;Depends on&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Window&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Instance&lt;/td&gt;
&lt;td&gt;nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Surface&lt;/td&gt;
&lt;td&gt;Instance + Window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;Device&lt;/td&gt;
&lt;td&gt;Instance + Surface&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Swapchain&lt;/td&gt;
&lt;td&gt;Device + Surface&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Destruction is the reverse — you always tear down in the opposite order of creation, so nothing tries to use something that's already gone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;surface&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;renderer&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;  &lt;span class="c1"&gt;// swapchain → device → instance&lt;/span&gt;
    &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Destroy&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're coming from JavaScript, you might wonder: why do we need to call &lt;code&gt;Destroy()&lt;/code&gt; at all? In JS, you just stop referencing an object and the &lt;strong&gt;garbage collector&lt;/strong&gt; eventually frees the memory. C++ has no garbage collector. When you allocate a resource — a Vulkan device, a window, a block of GPU memory — it stays allocated until &lt;em&gt;you&lt;/em&gt; explicitly release it. If you forget, it leaks: the resource is gone from your program's perspective but still held by the OS or GPU driver until the process exits. Every &lt;code&gt;Destroy()&lt;/code&gt; method you see in this engine is doing what the garbage collector would do for you in JavaScript — but manually and in a specific order, because these resources depend on each other.&lt;/p&gt;

&lt;p&gt;The game loop is simple for now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="n"&gt;Application&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ShouldClose&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PollEvents&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The application owns the loop. The window just reports events. The renderer doesn't know about the loop at all. As the engine grows, the loop will expand to include scene updates and render calls, but the structure stays the same.&lt;/p&gt;

&lt;p&gt;The demo application is intentionally minimal — two lines of real code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cpp"&gt;&lt;code&gt;&lt;span class="kt"&gt;int&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;Application&lt;/span&gt; &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"Ultra 3D Engine"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EXIT_SUCCESS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the whole point of layered architecture. The complexity of Vulkan initialization, GPU selection, swapchain creation — none of that leaks into the application code. It's all behind &lt;code&gt;Application&lt;/code&gt;, exactly where it should be.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;At this point we have a window, a Vulkan instance, a surface, a device, and a swapchain. The infrastructure is in place. We have images to render to and a GPU ready to receive commands.&lt;/p&gt;

&lt;p&gt;In the next part, we'll create the &lt;strong&gt;render pass&lt;/strong&gt; and &lt;strong&gt;graphics pipeline&lt;/strong&gt; — the point where we actually tell the GPU &lt;em&gt;what&lt;/em&gt; to draw.&lt;/p&gt;

</description>
      <category>cpp</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>From Rigidity to Explicitness: How AI Changes the Role of Constraints in Software</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Tue, 05 May 2026 20:21:12 +0000</pubDate>
      <link>https://forem.com/remojansen/from-rigidity-to-explicitness-how-ai-changes-the-role-of-constraints-in-software-5cp5</link>
      <guid>https://forem.com/remojansen/from-rigidity-to-explicitness-how-ai-changes-the-role-of-constraints-in-software-5cp5</guid>
      <description>&lt;p&gt;For a long time, software engineering has talked about a familiar trade-off: &lt;strong&gt;rigid vs flexible systems&lt;/strong&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Static typing vs dynamic typing
&lt;/li&gt;
&lt;li&gt;Relational databases vs schema-less storage
&lt;/li&gt;
&lt;li&gt;Strict architectures vs “just ship it” codebases
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The usual story is simple: rigidity slows you down, flexibility speeds you up.&lt;/p&gt;

&lt;p&gt;But this framing is becoming less useful.&lt;/p&gt;

&lt;p&gt;In the context of AI-assisted development, the real axis of optimization is shifting away from rigidity vs flexibility and toward something more precise:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;implicit systems vs explicit systems&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that shift changes how we should think about many foundational technologies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The old model: speed vs structure
&lt;/h2&gt;

&lt;p&gt;Traditionally, we’ve optimized software systems around a few competing forces:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Solo developers prefer speed and flexibility&lt;/li&gt;
&lt;li&gt;Teams prefer structure and predictability&lt;/li&gt;
&lt;li&gt;Large systems gradually accumulate constraints to manage complexity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is why tools like Node.js have thrived for rapid iteration, while others like TypeScript gained adoption as teams and codebases scaled.&lt;/p&gt;

&lt;p&gt;Similarly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;NoSQL systems optimized for flexibility and scaling&lt;/li&gt;
&lt;li&gt;SQL systems optimized for consistency and relational integrity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The underlying assumption was always the same:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Structure slows you down locally, but helps you scale globally.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That assumption is still true—but incomplete.&lt;/p&gt;




&lt;h2&gt;
  
  
  The missing variable: AI changes the bottleneck
&lt;/h2&gt;

&lt;p&gt;AI tools fundamentally change what part of the development process is expensive.&lt;/p&gt;

&lt;p&gt;Writing code is becoming cheaper.&lt;/p&gt;

&lt;p&gt;What remains expensive is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;verifying correctness
&lt;/li&gt;
&lt;li&gt;ensuring integration consistency
&lt;/li&gt;
&lt;li&gt;understanding system behavior
&lt;/li&gt;
&lt;li&gt;validating assumptions across components
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In other words:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;the bottleneck is shifting from generation to verification&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And that shift matters more than it first appears.&lt;/p&gt;

&lt;p&gt;Because once verification becomes the limiting factor, the value of “good structure” changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The rise of underused verification practices
&lt;/h2&gt;

&lt;p&gt;If AI reduces the cost of producing code, then the relative value of practices that &lt;strong&gt;verify, constrain, and stabilize systems&lt;/strong&gt; increases.&lt;/p&gt;

&lt;p&gt;This doesn’t just affect language or database choices—it also changes which engineering practices become mainstream.&lt;/p&gt;

&lt;p&gt;Techniques that were historically seen as “too expensive” or “overkill” in many teams may become default:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;architecture-level unit testing (validating system boundaries, not just functions)
&lt;/li&gt;
&lt;li&gt;stronger contract testing between services
&lt;/li&gt;
&lt;li&gt;more explicit dependency boundaries and enforcement
&lt;/li&gt;
&lt;li&gt;formalized integration testing as a first-class design tool
&lt;/li&gt;
&lt;li&gt;schema- and contract-driven development workflows
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Many of these practices already exist, but they are often underused because they slow down early iteration.&lt;/p&gt;

&lt;p&gt;However, in an AI-assisted environment:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;generating code becomes cheap, but validating system correctness does not.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So the bottleneck shifts toward practices that reduce ambiguity and enforce correctness at system boundaries.&lt;/p&gt;

&lt;p&gt;In that context, what used to feel like “over-engineering” starts to look like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;necessary structure for managing AI-generated complexity&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Rigidity is the wrong abstraction
&lt;/h2&gt;

&lt;p&gt;The term “rigid” bundles together several different properties:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicitness
&lt;/li&gt;
&lt;li&gt;constraint density
&lt;/li&gt;
&lt;li&gt;flexibility loss
&lt;/li&gt;
&lt;li&gt;verification ease
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not the same thing.&lt;/p&gt;

&lt;p&gt;A better distinction is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Implicit systems&lt;/strong&gt; → rely on inference, conventions, and runtime discovery
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Explicit systems&lt;/strong&gt; → encode structure, constraints, and intent directly
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters because AI systems don’t “understand” code in a human sense—they infer meaning from signals.&lt;/p&gt;

&lt;p&gt;So the question becomes:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How much of the system’s intent is explicitly encoded vs left to inference?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  Why explicit systems matter more in an AI-assisted world
&lt;/h2&gt;

&lt;p&gt;AI systems are especially good at pattern completion, but weaker at:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;resolving ambiguous intent
&lt;/li&gt;
&lt;li&gt;inferring hidden constraints reliably
&lt;/li&gt;
&lt;li&gt;maintaining global consistency across evolving systems
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So explicit systems act as:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;high-signal context providers for both humans and AI&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A SQL schema provides a machine-readable model of relationships
&lt;/li&gt;
&lt;li&gt;Type systems provide executable contracts for data flow
&lt;/li&gt;
&lt;li&gt;API specs define integration boundaries unambiguously
&lt;/li&gt;
&lt;li&gt;effect systems make side effects visible instead of implicit
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In this framing, constraints are not overhead.&lt;/p&gt;

&lt;p&gt;They are &lt;strong&gt;context compression mechanisms&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete implication: revisiting “flexibility-first” choices
&lt;/h2&gt;

&lt;p&gt;If verification becomes the dominant cost in development, then systems that maximize explicit structure become increasingly valuable.&lt;/p&gt;

&lt;p&gt;This leads to a natural reevaluation of several long-standing trade-offs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Relational databases over schema-less designs when correctness and reasoning matter
&lt;/li&gt;
&lt;li&gt;Static typing over dynamic typing for improving integration safety and AI-assisted code generation
&lt;/li&gt;
&lt;li&gt;Explicit effect systems (as seen in functional programming patterns) for making side effects observable
&lt;/li&gt;
&lt;li&gt;Contract-based communication (e.g. gRPC-style interfaces) over loosely structured HTTP conventions
&lt;/li&gt;
&lt;li&gt;Actor-model-style architectures for isolating state and making concurrency behavior explicit
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not a claim that one category replaces another universally.&lt;/p&gt;

&lt;p&gt;It is a shift in evaluation criteria:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How easily can correctness be verified by both humans and machines?&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  SQL vs NoSQL is not about rigidity
&lt;/h2&gt;

&lt;p&gt;Take databases as an example.&lt;/p&gt;

&lt;p&gt;Relational systems like SQL are often described as “rigid” because they enforce schemas and constraints.&lt;/p&gt;

&lt;p&gt;But another way to see them is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;they make assumptions explicit and queryable&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That explicitness has downstream effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;relationships are defined, not inferred
&lt;/li&gt;
&lt;li&gt;constraints are enforced, not assumed
&lt;/li&gt;
&lt;li&gt;structure is inspectable by tools and systems
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;NoSQL systems trade some of this explicitness for flexibility and schema evolution speed.&lt;/p&gt;

&lt;p&gt;Neither is inherently better.&lt;/p&gt;

&lt;p&gt;But in an AI-assisted workflow, explicit structure becomes more valuable because it reduces the cost of verification and integration.&lt;/p&gt;




&lt;h2&gt;
  
  
  Static vs dynamic typing: a similar shift
&lt;/h2&gt;

&lt;p&gt;The same pattern appears in typing systems.&lt;/p&gt;

&lt;p&gt;A static system like TypeScript encodes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Data shape&lt;/span&gt;
&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// Interface contract&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// implementation&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Integration constraint&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;User&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A dynamic system like JavaScript leaves much of that implicit until runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// No enforced structure&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;getUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`/api/users/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Assumes shape at runtime&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendEmail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// may fail if email is missing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In a human-only workflow, both are viable depending on team size and discipline.&lt;/p&gt;

&lt;p&gt;But in an AI-assisted workflow, static structure becomes more than safety—it becomes &lt;strong&gt;machine-readable intent&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The deeper shift: from human-readable to machine-verifiable systems
&lt;/h2&gt;

&lt;p&gt;What’s actually changing is not that constraints are becoming more important in general.&lt;/p&gt;

&lt;p&gt;It’s that constraints are now serving a second audience:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;AI systems that need structured, unambiguous context to assist effectively.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This introduces a new design pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;not just “is this readable for humans?”&lt;/li&gt;
&lt;li&gt;but “is this verifiable and interpretable for machines?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That changes how we evaluate architecture, APIs, and even language design.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: toward explicit-by-design systems
&lt;/h2&gt;

&lt;p&gt;The old framing of software trade-offs as rigidity vs flexibility is becoming less useful.&lt;/p&gt;

&lt;p&gt;A more accurate model is emerging:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;implicit inference vs explicit structure&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;AI shifts the balance because it reduces the cost of generating code—but increases the importance of verifying it.&lt;/p&gt;

&lt;p&gt;In that world, the most effective systems are not necessarily the most flexible or the most rigid.&lt;/p&gt;

&lt;p&gt;They are the ones that are:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;explicit enough to be verifiable, but not so constrained that they reduce productive expression&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That balance—not rigidity—is what will define good architecture in the AI-assisted era.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>softwaredevelopment</category>
      <category>software</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Source code is now a common good, and SaaS is mostly dead</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 27 Mar 2026 23:26:48 +0000</pubDate>
      <link>https://forem.com/remojansen/source-code-is-now-a-common-good-and-saas-is-mostly-dead-gke</link>
      <guid>https://forem.com/remojansen/source-code-is-now-a-common-good-and-saas-is-mostly-dead-gke</guid>
      <description>&lt;p&gt;Back in 2023, I wrote a post titled &lt;a href="https://dev.to/wolksoftware/the-upcoming-saas-bubble-burst-1c95"&gt;"The upcoming SaaS bubble burst"&lt;/a&gt; where I argued that AI would enable individual developers to replicate SaaS products at a fraction of the cost, turning high-margin businesses into commodities.&lt;/p&gt;

&lt;p&gt;I was wrong about the timeline. It's happening right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Legal Hack That Started It All
&lt;/h2&gt;

&lt;p&gt;In 1982, a company called Phoenix Technologies wanted to create IBM PC-compatible computers. The problem? IBM's BIOS was copyrighted. Their solution was a "clean room" process: one team reverse-engineered the BIOS and wrote a functional specification, then a completely separate team—who had never seen the original code—implemented a new BIOS from scratch based solely on that specification.&lt;/p&gt;

&lt;p&gt;It worked. Courts ruled it legal because the second team independently created the code; they never copied anything. This became established legal precedent.&lt;/p&gt;

&lt;p&gt;Fast forward to March 2025: a satirical (but functional) service called &lt;a href="https://malus.sh" rel="noopener noreferrer"&gt;Malus.sh&lt;/a&gt; appeared, offering "Clean Room as a Service." The name itself—Latin for "evil"—was a joke, but the concept was deadly serious. Pay them, upload your dependency manifest, and their AI systems would reimplement your open source dependencies under any license you wanted. GPL becomes MIT. AGPL becomes proprietary.&lt;/p&gt;

&lt;p&gt;The service was presented at FOSDEM as a warning to the open source community. The &lt;a href="https://news.ycombinator.com/item?id=47350424" rel="noopener noreferrer"&gt;Hacker News discussion&lt;/a&gt; exploded with over 1,400 comments debating whether this was the end of copyleft or just a clever thought experiment.&lt;/p&gt;

&lt;p&gt;Around the same time, someone attempted exactly this with the &lt;a href="https://github.com/chardet/chardet/issues/327" rel="noopener noreferrer"&gt;chardet&lt;/a&gt; Python library—using Claude to rewrite it from LGPL to MIT. The open source community was furious.&lt;/p&gt;

&lt;p&gt;But here's what truly terrifies me: this is unstoppable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Two Directions of Liberation
&lt;/h2&gt;

&lt;p&gt;There are two scenarios playing out simultaneously:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Open Source → Commercial (GPL → MIT → Proprietary)
&lt;/h3&gt;

&lt;p&gt;Companies have always hated copyleft licenses like GPL and AGPL because they require sharing modifications. If you can reimplement the software cleanly, you eliminate that obligation. No more "viral" licensing concerns. No more open source compliance headaches.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Commercial → Open Source
&lt;/h3&gt;

&lt;p&gt;This is the scenario most people aren't talking about. SaaS applications don't expose their source code, but they do expose their behavior. Every button click, every API response, every error message is observable. Feed that to an LLM, produce a specification, have another LLM implement it. The source code was never "seen."&lt;/p&gt;

&lt;p&gt;For SaaS, the clean room defense is even stronger because there's literally no source code to contaminate the process.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Is Unstoppable
&lt;/h2&gt;

&lt;p&gt;I know what you're thinking: "But the LLMs were trained on that code! They've seen it!"&lt;/p&gt;

&lt;p&gt;You're right. Someone demonstrated that Claude can reproduce chardet's source code &lt;strong&gt;verbatim from memory&lt;/strong&gt;, including the license headers. The training data is contaminated.&lt;/p&gt;

&lt;p&gt;But here's the thing: the architecture can be fixed.&lt;/p&gt;

&lt;p&gt;Imagine a two-LLM system:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM 1&lt;/strong&gt; ("dirty room"): Analyzes only public documentation, API specs, README files, type definitions. Never sees source code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM 2&lt;/strong&gt; ("clean room"): Trained specifically WITHOUT the target project's code. Receives only the specification from LLM 1.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Or a three-LLM system. Or more. Each hop makes the provenance harder to trace. Add different base models, different hosting providers, different jurisdictions. Good luck proving in court that the final output was "copied."&lt;/p&gt;

&lt;p&gt;For SaaS, it's even simpler. Point an AI agent at the UI. Let it click around, observe responses, document behavior. Feed that documentation to a clean model. Where's the infringement?&lt;/p&gt;

&lt;p&gt;The legal standard for clean room implementation was designed for humans with imperfect memories working for months. When AI can do it in hours with perfect documentation, the entire framework breaks down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Government Dilemma
&lt;/h2&gt;

&lt;p&gt;Governments will eventually have to respond. But their options are all bad:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Software patents&lt;/strong&gt;: The only real way to protect ideas (not just expression) is patents. But broad software patents would stall innovation even more than copyright does. We already tried this in the 2000s with patent trolls. Nobody wants to go back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stronger copyright enforcement&lt;/strong&gt;: Unenforceable across borders. The moment one country allows this, the "liberated" code can be downloaded legally from servers in that jurisdiction.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do nothing&lt;/strong&gt;: Tech companies lose their moats. VC-funded SaaS collapses.&lt;/p&gt;

&lt;p&gt;I expect different regions to react differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The US&lt;/strong&gt; has strong tech lobbying, but also a free-market ideology that resists new restrictions.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The EU&lt;/strong&gt; tends to prioritize consumer interests over corporate ones. Cheaper software sounds pretty good to voters.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Other countries&lt;/strong&gt; have less to lose and may deliberately permit this to erode Western IP advantages.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At the end of the day, if one country in the world allows AI-powered clean room reimplementation, you'll be able to download that "liberated" code from that country legally.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Ironic Outcome: Everything Becomes Open Source
&lt;/h2&gt;

&lt;p&gt;Here's the twist I didn't see coming: GPL exists to ensure software stays free forever. Richard Stallman created it because he believed users should have the freedom to run, study, modify, and share software.&lt;/p&gt;

&lt;p&gt;AI clean rooms might achieve exactly that outcome—just not in the way anyone intended.&lt;/p&gt;

&lt;p&gt;Think about it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Company A takes GPL software and relicenses it as proprietary.&lt;/li&gt;
&lt;li&gt;Company B takes Company A's proprietary software and reimplements it as open source.&lt;/li&gt;
&lt;li&gt;Company C takes that and makes it proprietary again.&lt;/li&gt;
&lt;li&gt;Repeat forever.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The end state? Everything converges to effectively public domain. You can't maintain a proprietary advantage when anyone can recreate your software in hours. You can't maintain copyleft when it can be circumvented just as easily.&lt;/p&gt;

&lt;p&gt;Source code becomes a common good. Not because of ideology, but because protection becomes technically impossible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Can You Actually Sell?
&lt;/h2&gt;

&lt;p&gt;When code has no scarcity, what's left?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Convenience.&lt;/strong&gt; That's it.&lt;/p&gt;

&lt;p&gt;You can run the software yourself for free. Set up your own servers, handle updates, manage disaster recovery, deal with security patches—or pay someone a monthly fee to do it for you.&lt;/p&gt;

&lt;p&gt;But here's the problem: when self-hosting becomes trivial (thanks to agentic SRE and AI-powered infrastructure management), convenience becomes less valuable. Margins compress. The race to the bottom accelerates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Winners and Losers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The Real Winners: Cloud Infrastructure Providers&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AWS, GCP, Azure. They own the hardware, the data centers, the network infrastructure. Whether you're running open source or proprietary software, you're paying them for compute. They win regardless of who owns the code.&lt;/p&gt;

&lt;p&gt;In the short term, there might be shock to the system if an AI company like OpenAI fails spectacularly. That could damage cloud valuations temporarily. But long term? I'm bullish.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Losers: Pure SaaS Companies&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Any SaaS company whose value proposition is primarily "we wrote software that does X" is in trouble. If your moat is code, you no longer have a moat.&lt;/p&gt;

&lt;p&gt;And here's the brutal reality: most SaaS companies were never designed for low-margin economics. They have massive operating costs—large engineering teams, expensive offices, bloated sales organizations, venture debt—all predicated on the assumption of 70-80% gross margins. When margins collapse to 20-30% (or lower), the math simply doesn't work. They can't cut costs fast enough. Many will go bankrupt.&lt;/p&gt;

&lt;p&gt;This isn't a slow decline. SaaS companies with high burn rates and no data moat will face an existential crisis the moment a credible open source alternative appears. And with AI, that alternative can appear overnight.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The Exception: Data Moats&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Some SaaS companies will survive—specifically, the ones that control proprietary data that can't be recreated:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LinkedIn&lt;/strong&gt;: The value is the network graph, not the code&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bloomberg&lt;/strong&gt;: Proprietary real-time financial feeds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Palantir&lt;/strong&gt;: Government contracts + classified data access&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Credit bureaus&lt;/strong&gt;: Decades of credit history data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Snowflake&lt;/strong&gt;: Data gravity—once your data is there, leaving is painful&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your software generates or aggregates unique data that can't be reproduced, you have something AI can't replicate. If your software is just features, you're toast.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Next Frontier: Open Source Hardware
&lt;/h2&gt;

&lt;p&gt;I cannot speak for Richard Stallman, but I believe the vision behind GPL was to ensure that software would be free forever—like in the early days of computing when code was shared openly.&lt;/p&gt;

&lt;p&gt;AI-powered clean rooms may finally get us there, ironically through market forces rather than ideology.&lt;/p&gt;

&lt;p&gt;The next frontier is open source hardware. The designs can be liberated just like software, but manufacturing remains controlled by corporations with factories, supply chains, and regulatory approvals. Still, open hardware could finally kill proprietary drivers—another piece of the dream.&lt;/p&gt;

&lt;p&gt;I personally believe this trajectory is better for consumers. Software becomes cheaper. Innovation accelerates. The tax on digital goods imposed by intellectual property regimes gradually evaporates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The SaaS bubble I predicted in 2023 is bursting. The mechanism is just different than I expected—not market forces alone, but a fundamental breakdown in the ability to protect software as property.&lt;/p&gt;

&lt;p&gt;In 5-10 years, I expect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Most software to be effectively open source (by force or by choice)&lt;/li&gt;
&lt;li&gt;SaaS margins to collapse except for data-moat businesses&lt;/li&gt;
&lt;li&gt;Cloud infrastructure to be the dominant value capture layer&lt;/li&gt;
&lt;li&gt;The concept of "software ownership" to feel as quaint as owning individual MP3 files&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Is this good? Is this bad? Honestly, I don't know. But I do know it's happening. And the teams that prepare for a world where code has no scarcity will be the ones that thrive.&lt;/p&gt;

&lt;p&gt;What do you think? Am I too pessimistic about SaaS? Too optimistic about open source? I'd love to hear your perspective in the comments.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;If you found this interesting, you might also enjoy my previous posts on &lt;a href="https://dev.to/remojansen/agent-driven-development-add-the-next-paradigm-shift-in-software-engineering-1jfg"&gt;Agent Driven Development&lt;/a&gt; and &lt;a href="https://dev.to/wolksoftware/the-upcoming-saas-bubble-burst-1c95"&gt;the original SaaS bubble prediction from 2023&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Infinite Loop Part III: Agentic Software Engineering</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 27 Mar 2026 22:18:46 +0000</pubDate>
      <link>https://forem.com/remojansen/the-infinite-loop-part-iii-agentic-software-engineering-1h5f</link>
      <guid>https://forem.com/remojansen/the-infinite-loop-part-iii-agentic-software-engineering-1h5f</guid>
      <description>&lt;h2&gt;
  
  
  Creating a culture of trust, ownership, and data-driven continuous experimentation—now accelerated by AI
&lt;/h2&gt;

&lt;p&gt;The Infinite Loop (L∞P) was introduced in 2023 as a software development methodology that unified lessons from Agile, Lean UX, Kanban, DevOps, and Product-led growth. Its core philosophy—trust, ownership, outcomes over outputs, no arbitrary deadlines—was designed to create high-performance teams that could achieve flow state and deliver genuine customer value.&lt;/p&gt;

&lt;p&gt;Three years later, AI has fundamentally changed how software is built. Large language models, agentic workflows, and AI-assisted development have compressed the time from idea to implementation. What took days now takes hours; what took hours now takes minutes.&lt;/p&gt;

&lt;p&gt;But the principles of L∞P are not obsolete—they are more relevant than ever.&lt;/p&gt;

&lt;p&gt;In 2023, the core problem was that companies underinvested in discovery and used time boxes that corrupted quality. Teams rushed to build without proper validation, and artificial deadlines led to technical debt accumulation and output-over-outcome thinking.&lt;/p&gt;

&lt;p&gt;AI doesn't change this—it amplifies it. Teams that skip discovery will now ship bad products faster. The new constraint is verification: AI generates code quickly, but proving correctness requires human judgment and automation investment. The teams that automate verification fastest will ship fastest.&lt;/p&gt;

&lt;p&gt;This update to L∞P acknowledges this shift while preserving the core philosophy that made it effective.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl2mqtxm52qv5x6t46t8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ftl2mqtxm52qv5x6t46t8.png" alt=" " width="800" height="412"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  How AI Accelerates the Loop
&lt;/h2&gt;

&lt;p&gt;AI transforms every phase of the product development cycle—but humans remain in control of judgment and validation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Research &amp;amp; Discovery&lt;/strong&gt;: AI synthesizes market data, customer feedback, and competitive intelligence faster than manual research. Teams can explore more hypotheses with the same effort.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Prototyping&lt;/strong&gt;: AI generates functional prototypes that enable real user validation faster and more effectively than static UX mockups. Users interact with working software, not wireframes—leading to higher-quality feedback earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Discovery → Development Transition&lt;/strong&gt;: AI assists the translation from validated discovery to technical specification. It asks refinement questions, identifies gaps in implementation plans, surfaces edge cases, and highlights integration risks before development begins.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation&lt;/strong&gt;: AI accelerates code generation, but humans own architecture decisions and review all output. The role shifts from typing to steering and verifying.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verification&lt;/strong&gt;: AI cannot be 100% trusted with verification, but it accelerates it significantly—automated vulnerability scanning, test generation, code review assistance, and anomaly detection. Human judgment remains the final gate.&lt;/p&gt;

&lt;p&gt;AI compresses time but does not replace human judgment. Discovery still requires validation with real users. Architecture still requires human design. Verification still requires human oversight. AI makes the loop faster—it doesn't make corners safe to cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acceleration works both ways.&lt;/strong&gt; If a team is leveraging AI correctly—investing in discovery, maintaining verification automation, addressing technical debt—everything works well and faster. But if things are going wrong, they go wrong faster too.&lt;/p&gt;

&lt;p&gt;Technical debt that was accumulating before AI? Now it accumulates twice as fast. Vulnerabilities that went unnoticed? They multiply faster. Poor architectural decisions? They propagate through the codebase before anyone catches them. Teams skipping discovery? They ship the wrong product to customers in record time.&lt;/p&gt;

&lt;p&gt;AI is an amplifier, not a corrector. It accelerates whatever trajectory you're already on. Teams with good practices win bigger. Teams with bad practices lose faster.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Actually Changed (And What Didn't)
&lt;/h2&gt;

&lt;p&gt;The fundamentals haven't changed as much as the hype suggests. The core problems remain: companies still underinvest in discovery, still use arbitrary deadlines, still optimise for outputs over outcomes. What changed is speed—iterations of the loop are faster, and estimation is even more useless than before.&lt;/p&gt;

&lt;p&gt;But there's a cultural shift that will separate winners from losers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The developer identity problem&lt;/strong&gt;: Developers have long lived by "talk is cheap, show me the code." Code is part of our identity. Letting go of code ownership is painful—even traumatic. We obsess over clean code, elegant abstractions, and technical excellence. This isn't inherently wrong; good code matters.&lt;/p&gt;

&lt;p&gt;But many developers fail to recognise "good bad code"—code that is technically excellent but ultimately harmful. Premature optimizations. Over-engineered abstractions. Architectural purity that overcomplicates the system and prevents faster value delivery. The code might be beautiful, but the user perceives no value, and the product loses to a scrappier competitor.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The uncomfortable truth&lt;/strong&gt;: The winning product has rarely been the one with the best code. It's been the one with the most value—whether that's better UX, more features, faster iteration, or simply showing up first. Technical debt accumulated by winners gets paid down later. Technical perfection pursued by losers never ships.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI acceleration&lt;/strong&gt;: In the new world, this dynamic intensifies. AI makes code generation cheap. Control freaks who obsess over every line will be left behind. And while it's fun to mock "vibe coders" who prompt their way to working software, some of them will win—because they start with value, not code perfection. The vibe coders who ship without any verification will fail fast. The winners are those who start with value AND invest enough in verification to sustain velocity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The new developer role&lt;/strong&gt;: One consequence of AI is that developers must become more heavily involved in discovery. When implementation is fast, understanding &lt;em&gt;what&lt;/em&gt; to build matters more than &lt;em&gt;how&lt;/em&gt; to build it. Developers who stay isolated from customers, business context, and user research will become bottlenecks—not because they're slow at coding, but because they lack the judgment to steer AI toward value. The developers who thrive will be those who invest in understanding the business, participate in user research, and can make product decisions on the fly.&lt;/p&gt;

&lt;p&gt;This doesn't mean quality doesn't matter. It means quality serves value, not the other way around. The best teams will use AI to iterate faster toward value while maintaining just enough quality to sustain velocity. The worst teams will use AI to generate perfect code that nobody wants.&lt;/p&gt;




&lt;h2&gt;
  
  
  L∞P Principles
&lt;/h2&gt;

&lt;p&gt;L∞P proposes eleven equally essential principles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Customer-Centric&lt;/strong&gt;: Everyone should be in constant direct contact with customers, understand their needs, and be obsessed with delivering value to them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Value-Driven&lt;/strong&gt;: The team is asked to deliver an outcome, not an output. The effectiveness and efficiency of the team is measured by the success of the customers, not by outputs (No Burn-down charts).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Product-Led&lt;/strong&gt;: Remove silos between marketing, sales, customer success, and the product team.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Trust &amp;amp; Ownership&lt;/strong&gt;: The product team is tasked with leading the customer to success and having total freedom to come up with the optimal solution.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flow-Friendly&lt;/strong&gt;: There must be at least 50% allocated focus time on the calendar every day. This applies to both deep-thinking architectural work and AI-orchestrated development—both require protection from interruption.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No Estimates or Time Boxes&lt;/strong&gt;: Use a pull-based system. Focus on one work item at a time. Discovery over planning. AI velocity is unpredictable, making estimation even less meaningful than before.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cost Tracking Over Velocity Tracking&lt;/strong&gt;: Instead of tracking velocity or estimating story points, track actual costs: team salaries, infrastructure, AI tokens. You know what you're spending weekly. Then measure outcomes: Activation Rate, Retention Rate, LTV, NPS, Feature Engagement. If customer satisfaction is improving or sustained, does velocity matter? The question isn't "how fast are we going?"—it's "are we delivering value relative to cost?" This reframes budget conversations from "will we hit the deadline?" to "is this investment generating returns?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Explicit Policies&lt;/strong&gt;: Use templates for agendas and artefacts to prevent deviation from your processes. This extends to AI governance—establish clear policies for security review and quality standards for AI-generated output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clear Goals&lt;/strong&gt;: The entire organisation should understand the business mission, vision, principles, and strategy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Data-Driven&lt;/strong&gt;: The decisions, direction, and work items are backed by data.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Pragmatic&lt;/strong&gt;: Making decisions based on what is best for the project rather than just optimising for individual preferences or technical ideals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Automation-First&lt;/strong&gt;: Invest in automation before features. The team that automates verification, testing, and deployment will experience the full productivity gains of AI. A feature without automated verification is incomplete. Technical debt is addressed organically as part of ongoing work, not accumulated in a separate backlog.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  L∞P Roles
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you want to go fast, go alone; if you want to go far, go together" - African proverb&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;L∞P tries to balance collaboration and working as a team, so we can attempt to achieve goals that are bigger than ourselves (go far) with focus and alone time so we can get into the zone and be super productive (go fast). When we work together, our goal should be to remove unknowns and enable autonomy; then, we can go our separate ways and get stuff done.&lt;/p&gt;

&lt;p&gt;The L∞P team structure is designed to ensure all disciplines are aligned and work without silos. Instead of having separate teams for product development, sales, marketing, and other functions, there is one cross-functional team in charge of discovery and delivery. This team integrates with sales and marketing by aligning goals and strategies around the product.&lt;/p&gt;

&lt;p&gt;Discovery and delivery are not separate silos. Developers can propose hypotheses, build prototypes, and participate in user research—they are not just implementers waiting for specifications. Similarly, UX and product can contribute to technical discussions. The entire team owns the full cycle from idea to validated, live product.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Product Manager&lt;/strong&gt; is a key role in this structure, taking on both the roles of product owner and scrum master. The Product Manager is responsible for leading the team, making decisions that impact the product, and ensuring the team delivers maximum customer value efficiently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UX&lt;/strong&gt; plays a crucial role in the product-led growth organisation, responsible for the design and usability of the product. The UX team works closely with the Product Manager and Engineering to ensure that the product is easy to use and meets the customer's needs. AI accelerates prototype generation; UX spends more time on user validation than wireframing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Architecture&lt;/strong&gt; creates the blueprint for the product and ensures technical coherence. This role becomes more critical in the AI era—AI generates code fast but makes poor architectural decisions. Humans must own system design.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Engineering&lt;/strong&gt; implements architecture decisions, builds verification systems, and maintains the product. The role shifts from "writing code" to "orchestrating AI, reviewing output, and building verification automation."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Sales &amp;amp; Marketing&lt;/strong&gt; represents business functions that influence product perception and customer expectations. These teams work closely with the Product Manager to align go-to-market strategy with product capabilities, ensuring promises match what the product delivers.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AI orchestration is not a separate role. Every team member incorporates AI assistance into their existing responsibilities organically.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Product Manager (PM)
&lt;/h2&gt;

&lt;p&gt;The role of the product manager is the most critical one in the product team—and AI makes it more critical, not less. The PM is often seen as the proving ground for future CEOs, as the success or failure of a product falls on their shoulders. It's therefore important that the PM role is reserved for the best talent, with a combination of technical expertise, deep customer and business knowledge, credibility among stakeholders, market and industry understanding, and a passion for the product.&lt;/p&gt;

&lt;p&gt;A PM must be smart, reactive, and persistent, with a deep respect for the product team. They should also be comfortable with using data and analytics tools to inform their decisions and drive the success of the product. The PM's main task is to ensure that only the most valuable work items reach the backlog, guiding the product team towards building solutions that deliver the greatest impact and customer value.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How AI transforms the PM role:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;AI amplifies PM leverage but also amplifies the cost of poor judgment. When the team can build anything fast, deciding &lt;em&gt;what&lt;/em&gt; to build becomes the primary bottleneck. The PM who chooses wrong wastes more resources faster.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Research at scale&lt;/strong&gt;: AI synthesizes market data, customer feedback, and competitive intelligence. The PM can explore more hypotheses and validate faster—but must still make the judgment calls about what matters.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Saying no becomes harder&lt;/strong&gt;: AI can generate infinite feature ideas, prototypes, and specifications. The PM must resist the temptation to build everything that's now "easy." The discipline of focus intensifies.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Specification quality gates&lt;/strong&gt;: AI assists the translation from validated discovery to technical specifications, asking refinement questions and identifying gaps. But the PM validates that the specification actually captures customer value—AI can generate coherent specs for useless features.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Verification strategy ownership&lt;/strong&gt;: Before work begins, the PM ensures a verification strategy exists. How will we know this feature works? How will we know users value it? AI accelerates verification, but the PM defines what "verified" means.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Faster feedback loops&lt;/strong&gt;: AI-generated prototypes enable real user validation in hours instead of weeks. The PM must be ready to act on feedback immediately—there's no hiding behind "we'll fix it next sprint."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The PM role becomes less about managing process and more about making decisions under uncertainty. The teams that win will have PMs who can synthesize information fast, say no confidently, and move from validated learning to shipped value without hesitation.&lt;/p&gt;




&lt;h2&gt;
  
  
  L∞P Artefacts
&lt;/h2&gt;

&lt;p&gt;In this section, we are going to take a look at the L∞P artefacts. We will mention common artefacts from other methodologies, clarify why we will not use them, and introduce some new ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Mission and vision&lt;/strong&gt;: The product mission and vision should be clearly articulated and documented. The team should not only know what the product aims to be but also what it is not aiming to be.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Unified Backlog&lt;/strong&gt;: A single backlog with tags to distinguish work types. We use a pull-based system—take the top item from the backlog. No separate backlogs for discovery and development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Sprint Backlog&lt;/strong&gt;: We don't use a Sprint Backlog because we don't use time boxes. We use a Work board and Work-in-progress limits to track our current focus.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Definition of done&lt;/strong&gt;: We don't allow custom definitions of done. Done means live and used by actual customers. If it's live, it was verified—verification is a prerequisite, not a separate checkbox.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Product Increment&lt;/strong&gt;: We don't use a Product Increment because we don't accept the idea of something being "potentially releasable". We release everything; if we are not going to release it, we don't build it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Sprint goal&lt;/strong&gt;: We don't use a Sprint goal because we don't have time boxes but also because our metrics are already focused on outcomes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;❌ &lt;strong&gt;Separate technical debt backlog&lt;/strong&gt;: Technical debt is addressed organically as part of ongoing work, not accumulated separately.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Explicit work policies&lt;/strong&gt;: We use Explicit work policies to ensure that nobody corrupts or deviates from our principles.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;User stories&lt;/strong&gt;: We use User Stories, but we are careful to avoid including specific implementation details or technical requirements (WHAT) to keep the focus on the user's needs and goals (WHO and WHY). Stories should keep the focus on the user, enable collaboration and drive creative solutions. AI may draft stories; humans refine them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Technical specifications&lt;/strong&gt;: When discovery outputs are validated (user-tested prototypes, research findings), they transform into technical specifications. AI assists this transformation—taking a validated prototype and generating a specification draft. Refinement sessions identify gaps: edge cases, integration points, security considerations, verification requirements. The specification is complete when the team has enough clarity to implement without constant clarification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Verification automation&lt;/strong&gt;: Unit tests, end-to-end tests, AI-assisted security reviews, and observability are first-class deliverables, not afterthoughts. Every feature ships with its verification.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ &lt;strong&gt;Outcome metrics over output metrics&lt;/strong&gt;: We don't use Output-based metrics like Burn-down &amp;amp; Burn-up charts, Lead time, Cycle time and Cumulative flow diagrams because they make people focus on outputs, not outcomes. We use outcomes-based metrics instead, like Activation Rate, Retention Rate, Lifetime Value (LTV), Net Promoter Score (NPS), Feature Engagement, Cohort Analysis &amp;amp; A/B Testing, Change Failure Rate, Employee satisfaction surveys, Employee turnover rate. We are careful with the activation rate because we understand that retention rate is a more reliable metric for customer value.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  L∞P Ceremonies
&lt;/h2&gt;

&lt;p&gt;In this section, we are going to take a look at the L∞P ceremonies. We will mention common ceremonies from other methodologies, clarify why we will not use them, and introduce some new ones.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;❌ We don't use &lt;strong&gt;Sprints&lt;/strong&gt; because a sprint is a time box, and we believe that time boxes lead to decreased quality and lower customer value, so we don't have any Sprint-based meetings. Including:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;❌ Sprint planning, &lt;/li&gt;
&lt;li&gt;❌ Sprint review and &lt;/li&gt;
&lt;li&gt;❌ Sprint retrospective. &lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;However, we value the principles behind the Sprint retrospective.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;❌ We don't host the &lt;strong&gt;Delivery planning and Risk review&lt;/strong&gt; meetings from Kanban because they strongly focus on outputs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host as many &lt;strong&gt;User research/testing sessions&lt;/strong&gt; as needed to validate hypotheses and generate product ideas. The entire team participates in the research phase, sales and development included. AI can significantly enhance these sessions—agents can help facilitate discussions, synthesize findings in real-time, or transform meeting transcriptions into structured insights, specifications, and hypothesis refinements.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We block 4 hours daily in people's calendars to ensure they can get into the zone and move fast. We call this the &lt;strong&gt;Do Not Disturb (DnD)&lt;/strong&gt; meeting. This protected time applies to both deep-thinking work and AI-orchestrated development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a &lt;strong&gt;daily stand-up&lt;/strong&gt; meeting, but we use meeting agendas to ensure they don't become a checkpoint. The goal is to resolve blockers and provide the team with the information required to act with autonomy for the rest of the day.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a monthly &lt;strong&gt;Flow review&lt;/strong&gt; meeting to reinforce a continuous improvement culture. This meeting includes: What verification gaps exist? What automation was added? What escaped our automated checks? How can we prevent similar escapes? AI can assist by analysing production incidents, identifying patterns across issues, and suggesting automation opportunities.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a monthly &lt;strong&gt;Show and Tell&lt;/strong&gt; meeting to enable conversation across teams, share research insights, and celebrate our achievements. This is a meeting to share knowledge with other teams and the wider business.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host monthly &lt;strong&gt;hackathons&lt;/strong&gt; to encourage the development team to generate product ideas and reinforce the involvement of the developers in the discovery phase.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;✅ We host a quarterly &lt;strong&gt;Strategy review&lt;/strong&gt; meeting to align the product teams with the leadership's mission, vision and strategy.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The core philosophy remains: trust leads to ownership, ownership leads to agility, agility plus protected focus time leads to flow. AI accelerates this cycle—it doesn't replace it.&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>ai</category>
      <category>agile</category>
      <category>softwaredevelopment</category>
    </item>
    <item>
      <title>The 6th SOLID Principle?</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sat, 21 Mar 2026 01:40:31 +0000</pubDate>
      <link>https://forem.com/remojansen/the-6th-solid-principle-4hjp</link>
      <guid>https://forem.com/remojansen/the-6th-solid-principle-4hjp</guid>
      <description>&lt;p&gt;I've been writing about the SOLID principles for over a decade. I built &lt;a href="https://inversify.io/" rel="noopener noreferrer"&gt;InversifyJS&lt;/a&gt; because of them, I wrote about &lt;a href="https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad"&gt;implementing them with the onion architecture&lt;/a&gt;, and just recently, I argued that they are &lt;a href="https://dev.to/remojansen/the-solid-principles-are-universal-1c9m"&gt;universal design principles&lt;/a&gt; that show up far beyond the world of object-oriented programming. But I've always felt something was missing — not from the principles themselves, but from the conversation around them.&lt;/p&gt;

&lt;p&gt;SOLID tells you how to write good components. It doesn't tell you how to compose them into a system that can change shape.&lt;/p&gt;

&lt;p&gt;Let me explain.&lt;/p&gt;




&lt;h2&gt;
  
  
  The gap
&lt;/h2&gt;

&lt;p&gt;Imagine you have a perfectly SOLID codebase. Your &lt;code&gt;UserRepository&lt;/code&gt; depends on an abstraction. Your &lt;code&gt;EmailService&lt;/code&gt; has a single responsibility. Your &lt;code&gt;OrderProcessor&lt;/code&gt; is open for extension but closed for modification. Everything is beautiful. You followed the five principles to the letter.&lt;/p&gt;

&lt;p&gt;Now your CTO walks in and says: "We need to split the user management into its own microservice."&lt;/p&gt;

&lt;p&gt;What happens? You start ripping things apart. You create a new project, move files, rewrite imports, create new entry points, set up new dependency wiring, and extract shared interfaces into a package. It takes weeks. The SOLID components themselves were fine — they didn't need to change. But the &lt;em&gt;structure&lt;/em&gt; around them did. The boundaries of your system were baked into the source code: into import paths, into entry points, into which files lived in which project, into hard-coded wiring inside modules.&lt;/p&gt;

&lt;p&gt;SOLID gave you pristine components. But pristine components in a rigid structure still give you a rigid system.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changes with the Composition Root
&lt;/h2&gt;

&lt;p&gt;Now imagine the same codebase, but this time every component is wired together via an IoC container, and all the wiring lives in a single place: the composition root.&lt;/p&gt;

&lt;p&gt;Your &lt;code&gt;OrderProcessor&lt;/code&gt; doesn't import &lt;code&gt;UserRepository&lt;/code&gt; directly. It declares a dependency on an abstraction. The composition root is where the abstraction meets its implementation. It's the only place in your entire codebase that knows which concrete classes exist and how they relate to each other.&lt;/p&gt;

&lt;p&gt;Here's the interesting part. In a monolith, you have one composition root that loads everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// monolith/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmsModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your CTO wants microservices. Instead of ripping the codebase apart, you create a new composition root for each service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/user-service/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;userModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// services/order-service/composition-root.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;authModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;orderModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;emailModule&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The components didn't change. Not a single line. The only thing that changed is &lt;em&gt;which composition root assembles which components&lt;/em&gt;. The boundaries of your system — what constitutes a deployable unit, which components belong together — were never defined by the components themselves. They were defined at the composition root level.&lt;/p&gt;

&lt;p&gt;I wrote a detailed case study of exactly this approach: &lt;a href="https://dev.to/remojansen/from-monolith-to-microservices-without-changing-one-line-of-code-thanks-to-the-power-of-inversion-57l6"&gt;From Monolith to Microservices without changing one line of code&lt;/a&gt;. In that project, the same codebase produced either a monolith or a set of microservices depending on which composition roots were used and how the CI/CD pipeline was configured. We even migrated from CosmosDB to PostgreSQL by creating a new IoC module and swapping it in the composition root — zero changes to existing code.&lt;/p&gt;




&lt;h2&gt;
  
  
  This is NOT the Composition Root pattern
&lt;/h2&gt;

&lt;p&gt;I want to be very precise here because the distinction matters.&lt;/p&gt;

&lt;p&gt;The Composition Root is a pattern, defined by Mark Seemann, that tells you &lt;em&gt;where&lt;/em&gt; to wire your dependencies: in a single place, as close to the application's entry point as possible. Seemann is clear that each deployable application should have exactly one composition root — and he's right.&lt;/p&gt;

&lt;p&gt;But the principle I'm trying to articulate is not about where you wire things up. It's about what the components &lt;em&gt;know&lt;/em&gt; about their own boundaries.&lt;/p&gt;

&lt;p&gt;Here's the difference:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The Composition Root pattern&lt;/strong&gt; says: "Wire everything in one place per application." It's a &lt;em&gt;mechanism&lt;/em&gt; — the &lt;em&gt;how&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The principle I'm proposing&lt;/strong&gt; says: "Your components should be boundary-agnostic. They should not know whether they're part of a monolith, a microservice, a plugin, or a serverless function. That decision should be made at the composition root level, never inside the components themselves."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seemann says multiple composition roots within a single application is an anti-pattern, and I agree — &lt;em&gt;per deployable artifact&lt;/em&gt;. But the whole point is that you should be free to create &lt;em&gt;new deployable artifacts&lt;/em&gt; with &lt;em&gt;new composition roots&lt;/em&gt; that select different subsets of the same boundary-agnostic components. The components don't change. Only the composition roots do.&lt;/p&gt;

&lt;p&gt;In other words: the Composition Root pattern gives you the tool. The principle gives you the &lt;em&gt;reason to use it this way&lt;/em&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Boundary Deferral Spectrum
&lt;/h2&gt;

&lt;p&gt;One mental model that has helped me think about this is a spectrum. The question is: &lt;em&gt;how late can you defer the decision about where your system's boundaries are?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Design time (worst).&lt;/strong&gt; Boundaries are baked into the source code. Components import each other directly. Which components belong to which service is decided by folder structure, project boundaries, and import paths. Restructuring means rewriting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Composition time (good).&lt;/strong&gt; Boundaries are defined in the composition root. The components themselves are boundary-agnostic. You can create different composition roots that assemble the same components into different shapes — monolith, microservices, or anything in between. Restructuring means writing a new composition root.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Build time (better).&lt;/strong&gt; The composition root reads environment variables or build arguments to decide which modules to load. A single codebase produces different deployable artifacts depending on build configuration. In my monolith-to-microservices project, a Dockerfile with &lt;code&gt;SERVICE_NAME&lt;/code&gt; and &lt;code&gt;SERVICE_ENTRY_POINT&lt;/code&gt; build arguments produced different microservice images from the exact same source code. Restructuring means changing a build argument.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runtime (most deferred).&lt;/strong&gt; The system discovers and loads components dynamically. Plugin architectures like MEF (.NET), OSGi (Java), or VS Code's extension system don't know their own boundaries until they're running. A plugin can be added or removed without the host application knowing about it in advance. The system's shape is defined by &lt;em&gt;what's present on disk&lt;/em&gt;, not by what's in the source code.&lt;/p&gt;

&lt;p&gt;Plugin systems are, in my opinion, the purest embodiment of SOLID in practice. They represent the logical extreme of this spectrum: boundaries deferred to the latest possible moment. Every plugin has a single responsibility. The system is open for extension (install a plugin) and closed for modification (the host doesn't change). Plugins are substitutable (swap one implementation for another). They interact through narrow, segregated interfaces. And everything depends on abstractions, with concrete implementations loaded at runtime.&lt;/p&gt;

&lt;p&gt;It's not a coincidence that the most long-lived, adaptable software systems in history — IDEs, browsers, operating systems — are all built on plugin architectures. They defer boundaries until runtime, and that gives them the ability to evolve in ways that no amount of up-front design could anticipate.&lt;/p&gt;




&lt;h2&gt;
  
  
  Horizontal and vertical slicing
&lt;/h2&gt;

&lt;p&gt;The boundary decisions I'm talking about come in two flavours, and both should be deferrable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Vertical slices&lt;/strong&gt; are about technical concerns: the HTTP layer, the business logic layer, the data access layer. In a well-structured onion architecture, these layers are already separated by abstractions. The composition root is what connects them. You can replace your entire data access layer (say, swap Sequelize for TypeORM, or SQL for a REST API) by changing the composition root — the business logic doesn't know or care.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal slices&lt;/strong&gt; are about user journeys or business capabilities: user management, order processing, content management, authentication. In a modular monolith, these are IoC modules. In a microservices architecture, each one becomes its own deployable artifact with its own composition root.&lt;/p&gt;

&lt;p&gt;The principle says: &lt;em&gt;both kinds of boundaries should be defined at the composition root level&lt;/em&gt;. Your components should not know whether &lt;code&gt;UserRepository&lt;/code&gt; and &lt;code&gt;OrderRepository&lt;/code&gt; live in the same process or in different services across a network. They shouldn't know whether the email service is an in-process function call or an HTTP request to a third-party API. That's a composition-time decision.&lt;/p&gt;

&lt;p&gt;When both vertical and horizontal boundaries are deferred, your system becomes something like a bag of Lego bricks. You can assemble them into a monolith (one big model), or split them into microservices (many small models), or land somewhere in between — all without modifying a single brick. Only the instruction manual changes.&lt;/p&gt;




&lt;h2&gt;
  
  
  The same idea in functional programming
&lt;/h2&gt;

&lt;p&gt;This is not just an OOP/IoC container thing. The same principle shows up in functional programming, expressed through different mechanisms.&lt;/p&gt;

&lt;p&gt;In ML-family languages, &lt;strong&gt;module functors&lt;/strong&gt; let you parameterise an entire module over its dependencies. A functor takes a module signature (an abstraction) and produces a concrete module. The "wiring" — which concrete module gets passed to which functor — happens at the call site, not inside the functor. The functor is boundary-agnostic; it declares what it needs but not where it comes from or what system it belongs to.&lt;/p&gt;

&lt;p&gt;Effect systems like &lt;strong&gt;ZIO&lt;/strong&gt; (Scala) take this even further. A ZIO program declares its dependencies as a type-level "environment" (&lt;code&gt;ZIO[UserRepo &amp;amp; EmailService, Error, Result]&lt;/code&gt;), and the layers that satisfy those dependencies are assembled separately. You can build different layer configurations for testing, for a monolith, or for distributed services. The program itself doesn't change — only the layer composition does.&lt;/p&gt;

&lt;p&gt;Even in simpler functional codebases, the practice of threading dependencies as function parameters (dependency injection via higher-order functions) achieves the same thing. A function &lt;code&gt;(fetchFn) =&amp;gt; (userId) =&amp;gt; fetchFn(/users/${userId})&lt;/code&gt; doesn't know if &lt;code&gt;fetchFn&lt;/code&gt; hits a local database, a remote API, or a mock. The boundary decision is deferred to whoever provides &lt;code&gt;fetchFn&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The mechanism is different — functors instead of IoC containers, layers instead of composition roots, higher-order functions instead of constructor injection — but the principle is identical: components should be boundary-agnostic, and the decision about what belongs together should live outside the components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Has this been said before?
&lt;/h2&gt;

&lt;p&gt;I want to be honest about prior art because this idea doesn't come from nowhere.&lt;/p&gt;

&lt;p&gt;Robert C. Martin came very close in his 2014 article on Clean Micro-service Architecture, where he wrote: &lt;em&gt;"The Deployment Model is a Detail"&lt;/em&gt; and &lt;em&gt;"A good architect defers the decision about how the system will be deployed until the last responsible moment."&lt;/em&gt; He even coined the term &lt;strong&gt;"Forced Ignorance"&lt;/strong&gt; — the idea that components should have no knowledge of their deployment context.&lt;/p&gt;

&lt;p&gt;The Lean Software Development movement formalised &lt;strong&gt;"Decide as Late as Possible"&lt;/strong&gt; (Poppendieck, 2003) as a general principle about deferring decisions until you have enough information to make them well.&lt;/p&gt;

&lt;p&gt;Mark Seemann's &lt;strong&gt;Composition Root&lt;/strong&gt; pattern provides the mechanism to make this work in practice.&lt;/p&gt;

&lt;p&gt;But none of these, as far as I can tell, explicitly say: &lt;em&gt;the boundaries of your system — which components are grouped together, what constitutes a vertical slice, what constitutes a horizontal slice, what constitutes a deployable unit — should be defined at the composition root level, not inside the components, and should be reconfigurable without modifying the components themselves.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Uncle Bob says deployment topology is a detail. But he still assumes the component &lt;em&gt;boundaries&lt;/em&gt; are defined in code (jars, DLLs, project structure). The Composition Root pattern tells you &lt;em&gt;where&lt;/em&gt; to wire, but not that the components should be deliberately designed to be boundary-agnostic. "Decide as Late as Possible" is a general principle that applies to everything, not specifically to system boundaries.&lt;/p&gt;

&lt;p&gt;What I'm proposing sits in the gap between these ideas.&lt;/p&gt;




&lt;h2&gt;
  
  
  So what do we call it?
&lt;/h2&gt;

&lt;p&gt;I've been going back and forth on this, and the name I keep coming back to is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;The Boundary Deferral Principle&lt;/strong&gt;: The components of a system should be boundary-agnostic. System boundaries — both vertical (technical layers) and horizontal (business capabilities) — should be defined at the composition root level, never inside the components themselves. Boundary decisions should be deferred as late as practically possible.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Or more concisely: &lt;em&gt;system boundaries are a detail&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;This follows Uncle Bob's own framing — the database is a detail, the framework is a detail, the deployment model is a detail — and extends it to its logical conclusion: the boundaries themselves are a detail too.&lt;/p&gt;




&lt;h2&gt;
  
  
  SOLID+ ?
&lt;/h2&gt;

&lt;p&gt;SOLID without this principle has always felt incomplete to me. SOLID gives you the bricks. But without the idea that boundaries should be deferred and defined at the composition root level, you end up cementing those bricks into a shape too early. And when the shape needs to change — and it always does — you're back to a rewrite.&lt;/p&gt;

&lt;p&gt;The teams I've seen get the most out of SOLID are the ones that, whether they know it or not, also follow this principle. Their applications are plugin systems in disguise. Their components don't know their own topology. Their boundaries are defined in one place, and changing that one place changes the shape of the entire system.&lt;/p&gt;

&lt;p&gt;Maybe that's the 6th principle. Or maybe it's just what happens when you take the other five seriously enough. I'd love to hear your thoughts!&lt;/p&gt;

</description>
      <category>softwareengineering</category>
      <category>architecture</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>The SOLID Principles are Universal</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 20 Mar 2026 23:44:49 +0000</pubDate>
      <link>https://forem.com/remojansen/the-solid-principles-are-universal-1c9m</link>
      <guid>https://forem.com/remojansen/the-solid-principles-are-universal-1c9m</guid>
      <description>&lt;p&gt;I've spent a significant portion of my career thinking about the SOLID principles. I wrote about them in the context of &lt;a href="https://dev.to/remojansen/implementing-the-onion-architecture-in-nodejs-with-typescript-and-inversifyjs-10ad"&gt;JavaScript and TypeScript&lt;/a&gt;, I built &lt;a href="https://inversify.io/" rel="noopener noreferrer"&gt;InversifyJS&lt;/a&gt; largely because of them, and I've had countless conversations with other developers about whether they belong in the JavaScript world at all. Over the years, many people have pushed back, arguing that SOLID is an object-oriented thing, that it only matters if you're writing Java or C#, and that it doesn't apply to their world.&lt;/p&gt;

&lt;p&gt;I respectfully disagree. In fact, the more I look around, the more I'm convinced that the SOLID principles are not about object-oriented programming at all. They are universal design principles that manifest everywhere, whether you realise it or not. You'll find them in CSS utility frameworks, in functional programming, and even in the way we're building AI agents today.&lt;/p&gt;

&lt;p&gt;Let me show you what I mean.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Tailwind CSS
&lt;/h2&gt;

&lt;p&gt;When Tailwind CSS first gained popularity, I remember many developers (including myself) being skeptical. Writing &lt;code&gt;class="flex items-center justify-between p-4 bg-white rounded-lg shadow-md"&lt;/code&gt; felt wrong. It looked like inline styles with extra steps. But the more I used it, the more I started to see something familiar. Tailwind is SOLID, and it doesn't even know it.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;A class should have only a single responsibility.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Tailwind, each utility class does exactly one thing. &lt;code&gt;text-center&lt;/code&gt; centers text. &lt;code&gt;p-4&lt;/code&gt; adds padding. &lt;code&gt;bg-blue-500&lt;/code&gt; sets a background colour. That's it. No side effects, no surprises. Compare this to a traditional CSS class like &lt;code&gt;.card&lt;/code&gt; that might set padding, margin, background, border-radius, box-shadow, font-size, and who knows what else. That's a God class. Tailwind avoids this entirely by ensuring every class has a single, well-defined responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Software entities should be open for extension, but closed for modification.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;You cannot modify &lt;code&gt;p-4&lt;/code&gt;. It is what it is. But you can extend the system by composing additional classes: &lt;code&gt;p-4 md:p-8 lg:p-12&lt;/code&gt;. You can also extend Tailwind's configuration to add your own spacing scale, your own colours, your own breakpoints, all without touching the existing utility classes. The existing system is closed for modification but wide open for extension. This is exactly how the open/closed principle is supposed to work.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In Tailwind, any spacing utility is interchangeable with another spacing utility. You can swap &lt;code&gt;p-4&lt;/code&gt; for &lt;code&gt;p-6&lt;/code&gt; and the system doesn't break. You can replace &lt;code&gt;bg-blue-500&lt;/code&gt; with &lt;code&gt;bg-red-500&lt;/code&gt; and everything continues to work as expected. The utilities follow a consistent contract: they accept the same kind of input (a class name on an element) and produce a predictable kind of output (a single CSS property change). Any class within a category can substitute for another in that category without breaking the layout system.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;Many client-specific interfaces are better than one general-purpose interface.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This is perhaps where Tailwind shines the brightest. Instead of one monolithic &lt;code&gt;.card&lt;/code&gt; class that bundles together layout, spacing, colour, typography, and shadow concerns, Tailwind gives you many small, focused utilities. You pick only the ones you need. Your element doesn't have to "implement" an interface it doesn't care about. A heading doesn't need to know about box shadows just because it happens to be inside a card. Each utility is a tiny, client-specific interface. You compose exactly what you need and nothing more.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;blockquote&gt;
&lt;p&gt;One should depend upon abstractions, not concretions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This one is more subtle in Tailwind, but it's there. When you use Tailwind's design tokens (e.g., &lt;code&gt;text-primary&lt;/code&gt;, &lt;code&gt;bg-surface&lt;/code&gt;, or spacing values like &lt;code&gt;p-4&lt;/code&gt;), you're depending on an abstraction rather than a concrete value. You don't write &lt;code&gt;color: #3B82F6&lt;/code&gt; directly. You write &lt;code&gt;text-blue-500&lt;/code&gt;, which is an abstraction that points to a value defined in your configuration. If you change your blue from &lt;code&gt;#3B82F6&lt;/code&gt; to &lt;code&gt;#2563EB&lt;/code&gt; in the config, every component that depends on &lt;code&gt;blue-500&lt;/code&gt; gets updated automatically. Your components depend on the abstraction (the token), not the concretion (the hex value).&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Functional Programming
&lt;/h2&gt;

&lt;p&gt;I've always found the relationship between SOLID and functional programming fascinating. Mark Seemann wrote something that stuck with me years ago:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"If you take the SOLID principles to their extremes, you arrive at something that makes Functional Programming look quite attractive."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I referenced this quote in my &lt;a href="https://www.wolksoftware.com/blog/the-current-state-of-dependency-inversion-in-javascript/" rel="noopener noreferrer"&gt;post about the state of dependency inversion in JavaScript&lt;/a&gt;, and it's something I still think about. The SOLID principles don't require classes or interfaces to be meaningful. They show up naturally in well-written functional code.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, the single responsibility principle is baked into the philosophy. Pure functions do one thing: they take an input and produce an output with no side effects. A function like &lt;code&gt;const double = (x) =&amp;gt; x * 2&lt;/code&gt; has a single, clear responsibility. The entire functional paradigm pushes you towards small, composable functions that each do one thing well. When people talk about "Unix philosophy" — do one thing and do it well — they are really talking about single responsibility.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, functions are closed for modification by default because pure functions are immutable transformations. You can't go in and change what &lt;code&gt;map&lt;/code&gt; does. But you can extend behaviour by composing functions together. Higher-order functions are the ultimate open/closed mechanism: &lt;code&gt;pipe(validate, transform, serialize)&lt;/code&gt; lets you extend a pipeline without modifying any of the individual functions. Function composition is extension without modification.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;p&gt;In functional programming, this manifests as the ability to swap one function for another as long as it respects the same type signature. If a pipeline expects a function &lt;code&gt;(a) =&amp;gt; b&lt;/code&gt;, you can substitute any function that satisfies that contract. This is exactly what makes functional programming so powerful for testing as well. You can replace a function that reads from a database with a function that returns mock data, and the rest of your pipeline doesn't care. Same signature, same contract, different implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;p&gt;Functional programming naturally avoids bloated interfaces because functions inherently have narrow, focused signatures. Instead of passing a God object with twenty properties to a function, you pass only what that function needs. A function &lt;code&gt;(name: string) =&amp;gt; string&lt;/code&gt; doesn't force you to provide an entire User object when all it needs is a name.&lt;/p&gt;

&lt;p&gt;But there's a more subtle connection here that I find particularly compelling: the functional programming preference for unary functions (functions that take a single argument) over functions with multiple arguments is, in my opinion, a form of interface segregation.&lt;/p&gt;

&lt;p&gt;Think about it. A function like &lt;code&gt;(userId: string, includeOrders: boolean, formatAsCSV: boolean, sendEmail: boolean) =&amp;gt; Result&lt;/code&gt; is the functional equivalent of one general-purpose interface. The caller is forced to know about and provide values for concerns it might not care about. Maybe I just want to fetch a user. Why do I need to tell the function whether to format as CSV or send an email? This function has a "fat interface" — it mixes multiple concerns into a single signature.&lt;/p&gt;

&lt;p&gt;Now consider the unary alternative. Currying transforms that one fat function into a chain of focused, single-argument functions: &lt;code&gt;(userId) =&amp;gt; (options) =&amp;gt; (formatter) =&amp;gt; Result&lt;/code&gt;. Each function in the chain has a narrow, segregated interface. But more importantly, partial application lets you stop at any point in the chain and get back a specialised function that only demands what the next consumer actually needs. You can pass &lt;code&gt;getUserById&lt;/code&gt; (already partially applied with default options) to a part of your code that only cares about fetching users and doesn't need to know formatting even exists.&lt;/p&gt;

&lt;p&gt;This is interface segregation at the function level. Instead of one function with a wide, general-purpose parameter list, you get many smaller, focused functions, each requiring only what it needs from its caller. The parallel to "many client-specific interfaces are better than one general-purpose interface" is almost exact, just expressed through function signatures rather than interface declarations.&lt;/p&gt;

&lt;p&gt;Libraries like Ramda take this philosophy to the extreme — every function is curried by default, which means every function in the library naturally presents the narrowest possible interface to its consumer at each step of application.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;p&gt;This one is beautiful in functional programming. Instead of depending on a concrete database module or HTTP client, functional code often takes its dependencies as function parameters. This pattern is so common it has a name: dependency injection via higher-order functions. Instead of importing a concrete implementation, you write &lt;code&gt;const getUser = (fetchFn) =&amp;gt; (id) =&amp;gt; fetchFn(&lt;/code&gt;/users/${id}&lt;code&gt;)&lt;/code&gt;. Your business logic depends on the abstraction (a function that fetches), not the concretion (the specific HTTP library). The caller decides what implementation to inject.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. AI Agent Development
&lt;/h2&gt;

&lt;p&gt;This is the one that made me want to write this post. Over the last couple of years, I've been spending more and more time building and thinking about AI agents. I wrote about &lt;a href="https://dev.to/remojansen/agent-driven-development-add-the-next-paradigm-shift-in-software-engineering-1jfg"&gt;Agent Driven Development&lt;/a&gt; and &lt;a href="https://dev.to/remojansen/how-will-the-agentic-web-change-the-web-as-we-know-it-23ih"&gt;the Agentic web&lt;/a&gt; previously, and one thing that keeps striking me is how the patterns that make agents work well are the exact same patterns we've been preaching in software engineering for decades. The SOLID principles aren't just relevant in AI agent development. They might be essential.&lt;/p&gt;

&lt;h3&gt;
  
  
  S — Single Responsibility Principle
&lt;/h3&gt;

&lt;p&gt;The best agentic systems I've built or worked with follow this principle strictly. Each agent in the system should have a single, well-defined responsibility. A &lt;code&gt;ResearchAgent&lt;/code&gt; should gather information. A &lt;code&gt;SummaryAgent&lt;/code&gt; should condense findings into a concise output. A &lt;code&gt;CodeReviewAgent&lt;/code&gt; should analyse code for issues. When you build one God agent that researches, summarises, writes code, reviews it, sends emails, AND updates a project tracker, the quality of every single one of those tasks degrades. The agent tries to be everything and ends up being mediocre at all of it. Single responsibility for agents isn't just good architecture; it directly affects the quality of the AI's output. Smaller, focused agents produce better results because the system prompt, the context window, and the reasoning are all concentrated on one job.&lt;/p&gt;

&lt;h3&gt;
  
  
  O — Open/Closed Principle
&lt;/h3&gt;

&lt;p&gt;A well-designed multi-agent system is one where you can add new agents without modifying the existing orchestration logic. The orchestrator that coordinates your agents should be closed for modification. You shouldn't have to rewrite the routing or coordination logic every time you need a new capability. Instead, you register a new specialised agent with its description and responsibilities, and the orchestrator can start delegating to it. The existing agents remain untouched. This is exactly the philosophy behind frameworks like LangGraph, CrewAI, and the Model Context Protocol (MCP) — they let you extend the system by plugging in new agents or servers rather than modifying the ones that already work.&lt;/p&gt;

&lt;h3&gt;
  
  
  L — Liskov Substitution Principle
&lt;/h3&gt;

&lt;p&gt;In a multi-agent system, you should be able to swap one agent for another as long as it fulfils the same contract. If your orchestrator delegates summarisation to a &lt;code&gt;SummaryAgent&lt;/code&gt;, you should be able to replace that agent with an improved version — perhaps one powered by a different model, or one that uses a different prompting strategy — without the orchestrator knowing or caring. The contract is: "give me text, I'll give you a summary." How the agent internally achieves that is its own business. This is also why it matters to have clean interfaces between agents. If your &lt;code&gt;SummaryAgent&lt;/code&gt; can be backed by GPT-4, Claude, or a fine-tuned local model, and the rest of the system works regardless, you've achieved Liskov substitution at the agent level.&lt;/p&gt;

&lt;h3&gt;
  
  
  I — Interface Segregation Principle
&lt;/h3&gt;

&lt;p&gt;When designing agentic systems, it's far better to have many focused agents than one Swiss Army knife agent. You should have a &lt;code&gt;DataExtractionAgent&lt;/code&gt;, a &lt;code&gt;ValidationAgent&lt;/code&gt;, and a &lt;code&gt;FormattingAgent&lt;/code&gt; rather than one monolithic &lt;code&gt;DataProcessingAgent&lt;/code&gt; that tries to handle everything based on a mode parameter. Why? Because each agent can have a tightly scoped system prompt, a focused set of examples, and a narrow context window. The LLM performs better when it has a clear, specific job. A general-purpose agent with a system prompt that reads like a novel ends up confused about which hat it's supposed to be wearing at any given moment. This is interface segregation applied to AI: many specialised agents with narrow responsibilities are better than one general-purpose agent that does everything poorly.&lt;/p&gt;

&lt;h3&gt;
  
  
  D — Dependency Inversion Principle
&lt;/h3&gt;

&lt;p&gt;This is critical in agent architecture. Your orchestrator should depend on abstractions of agents, not on concrete implementations. If your orchestration logic is tightly coupled to a specific agent that uses a specific model with a specific prompt template, you've made the whole system rigid. Instead, define what an agent looks like in abstract terms — it takes an input, it returns an output, it has a description of what it does — and let the orchestrator depend on that abstraction. The concrete agent (which model it uses, how it prompts, what tools it has access to) is an implementation detail that gets injected. Tomorrow you might want to replace your Claude-powered &lt;code&gt;ResearchAgent&lt;/code&gt; with one that calls a specialised fine-tuned model, or even a non-LLM heuristic agent. If your orchestrator depends on the abstraction, that swap is trivial. This is the same principle I was advocating for with InversifyJS years ago, and it's exactly the approach taken by agent frameworks that separate the agent interface from the agent implementation.&lt;/p&gt;




&lt;h2&gt;
  
  
  Full Circle
&lt;/h2&gt;

&lt;p&gt;I find it remarkable that the same five principles Robert C. Martin articulated decades ago for object-oriented design show up, without being forced, in a CSS utility framework, in functional programming, and in the way we're building AI agents in 2026. They weren't invented for OOP. They were discovered as properties of well-designed systems.&lt;/p&gt;

&lt;p&gt;I wrote &lt;a href="https://dev.to/remojansen/the-yin-yang-principle-2ccn"&gt;The problem with Dogma in Software&lt;/a&gt; a few years ago, and I still stand by it. We should never apply any principle dogmatically. But I think the SOLID principles have earned their place as something deeper than just "OOP best practices". They are universal design principles about managing complexity, achieving composability, and building systems that can evolve over time.&lt;/p&gt;

&lt;p&gt;Whether you're styling a button with Tailwind, piping functions together in Haskell, or orchestrating a fleet of specialised AI agents — if the design feels clean and maintainable, there's a good chance the SOLID principles are hiding in there somewhere, whether you invited them or not.&lt;/p&gt;

&lt;p&gt;Thanks for reading. I'd love to hear your thoughts!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>softwareengineering</category>
      <category>designpatterns</category>
    </item>
    <item>
      <title>Running the Copilot CLI in a WebGL-powered retro CRT terminal implemented by the Copilot CLI 🤯</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Fri, 30 Jan 2026 23:33:50 +0000</pubDate>
      <link>https://forem.com/remojansen/running-the-copilot-cli-in-a-webgl-powered-retro-crt-terminal-implemented-by-the-copilot-cli-297</link>
      <guid>https://forem.com/remojansen/running-the-copilot-cli-in-a-webgl-powered-retro-crt-terminal-implemented-by-the-copilot-cli-297</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/github-2026-01-21"&gt;GitHub Copilot CLI Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;A port of &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;Swordfish90/cool-retro-term&lt;/a&gt; (Qt and OpenGL) to WebGL, React, and Electron, and use to make my website look like a cool retro monochrome CRT monitor with an OS from 1977.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Please note:&lt;/strong&gt; The site is meant to be used with a keyboard (no mouse or touch controls).&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  My Experience with GitHub Copilot CLI
&lt;/h2&gt;

&lt;p&gt;I first had the idea of building a website that looks like an old-style monochrome CRT monitor when I discovered &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;Swordfish90/cool-retro-term&lt;/a&gt;. I loved it, but the problem is that cool-retro-term is a native application that uses Qt &amp;amp; OpenGL. I wondered if it would be possible to port the code to WebGL, but because I have no experience with Qt or OpenGL, answering this question would have taken me many hours.&lt;/p&gt;

&lt;p&gt;In the past, I wouldn't have pursued this project because I didn't have much time outside of work, but now, thanks to GitHub Copilot and its CLI, I was able to get a good understanding of how cool-retro-term works and put together a migration plan in minutes.&lt;/p&gt;

&lt;p&gt;Then, over a couple of evenings, I started to migrate the OpenGL shaders to WebGL one at a time, and soon I had a working port 🎉&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbx3cxn5421j0vtzl9cl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fcbx3cxn5421j0vtzl9cl.png" alt="https://remojansen.github.io/" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The original project (cool-retro-term) implemented an OpenGL frontend for an OS terminal. My website runs in a browser, so I had to implement a basic terminal emulator, and Copilot was able to do so without any problems.&lt;/p&gt;

&lt;p&gt;Because the rendering was decoupled from the terminal, I thought others could be interested in this as a library, so I released it as &lt;a href="https://github.com/remojansen/cool-retro-term-webgl" rel="noopener noreferrer"&gt;cool-retro-term-webgl&lt;/a&gt;, and I also thought that the most likely next usage would be an Electron-based version of cool-retro-term so I also added that to cool-retro-term-webgl.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Now I can run the Copilot CLI in a WebGL-powered retro CRT terminal implemented by the Copilot CLI 🤯&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrvdwohl760bmcvv9l8e.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxrvdwohl760bmcvv9l8e.png" alt="GitHub Copilot CLI in cool-retro-term-electron" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I was very happy with everything that I was able to accomplish in just a couple of days, and quite impressed by the power of the GitHub Copilot Agents together with Claude Opus 4.5. It was then that the fan really started. I started to add all sorts of cool and fun "programs" to my terminal emulator, and honestly, I have not had so much fun coding in a very long time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pbpi9ok26suyxbzpqx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi3pbpi9ok26suyxbzpqx.png" alt="Games in https://remojansen.github.io" width="800" height="457"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are a couple of fun easter eggs and nerdy references. I hope you enjoy them (maybe you can "hack" my cluster 😉).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>githubchallenge</category>
      <category>cli</category>
      <category>githubcopilot</category>
    </item>
    <item>
      <title>Turning Your Living Room into a Couch Co-op Arena with TouchCoop</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Mon, 19 Jan 2026 03:11:10 +0000</pubDate>
      <link>https://forem.com/remojansen/turning-your-living-room-into-a-couch-co-op-arena-with-touchcoop-249j</link>
      <guid>https://forem.com/remojansen/turning-your-living-room-into-a-couch-co-op-arena-with-touchcoop-249j</guid>
      <description>&lt;p&gt;Hey everyone 👋,&lt;/p&gt;

&lt;p&gt;Today I want to share something fun. Imagine this: you're at home with friends, the big TV is on, and instead of everyone fighting over Bluetooth controllers or passing around one gamepad… everyone just pulls out their phone and instantly becomes a player.&lt;/p&gt;

&lt;p&gt;No accounts. No installs. Just scan a QR code and start mashing buttons.&lt;/p&gt;

&lt;p&gt;That's exactly what &lt;strong&gt;TouchCoop&lt;/strong&gt; enables — a tiny TypeScript library that turns this vision into reality with almost zero server hassle.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Host runs the game on a laptop/TV-connected browser&lt;/li&gt;
&lt;li&gt;Up to 4 players join via QR code on their mobiles&lt;/li&gt;
&lt;li&gt;Touch buttons on phone → real-time input to the game via WebRTC&lt;/li&gt;
&lt;li&gt;Perfect for casual games: platformers, party games, local puzzles&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Not&lt;/strong&gt; suited for low-latency games like FPS (WebRTC latency is good but not esports-level)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Quick architecture overview
&lt;/h3&gt;

&lt;p&gt;Your game needs &lt;strong&gt;two&lt;/strong&gt; distinct entry points (URLs):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Match page&lt;/strong&gt; (TV/Laptop): Creates a &lt;code&gt;Match&lt;/code&gt; instance, shows QR codes for joining, and receives player events.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Gamepad page&lt;/strong&gt; (Phone): Opened via QR code, creates a &lt;code&gt;Player&lt;/code&gt; instance, connects, and sends touch events.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Both are just static web pages — no backend required beyond the signaling phase.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started in 60 seconds
&lt;/h3&gt;

&lt;p&gt;Install the library:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;touch-coop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  1. The Match side (your game)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;PlayerEvent&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;touch-coop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;gamePadURL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://your-domain.com/gamepad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// Must be absolute URL for QR&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;handlePlayerEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;PlayerEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;action&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;JOIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; joined 🎉`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Maybe spawn player avatar, play sound, etc.&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;LEAVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; left 😢`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;MOVE&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Player &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; → &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="c1"&gt;// Here you map "up", "A", "X" etc. to game actions&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;button&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nf"&gt;jumpPlayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;playerId&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;gamePadURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlePlayerEvent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;dataUrl&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createLobby&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;gamePadURL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;handlePlayerEvent&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Call &lt;code&gt;match.createLobby()&lt;/code&gt; to get the dataUrl of a QR Code that displays a virtual gamepad.&lt;/p&gt;

&lt;h4&gt;
  
  
  2. The Gamepad side (React example)
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight tsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Player&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;touch-coop&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Player&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="c1"&gt;// Can pass custom PeerJS config too&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;GamePad&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;joinMatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Player Name Here&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nf"&gt;setLoading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Failed to join&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;})();&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;[]);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"text-2xl"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Connecting...&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;;&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"grid grid-cols-3 gap-4 p-8 h-screen bg-black text-white"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;up&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;↑&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;left&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;←&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;right&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;→&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn col-start-2"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;down&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;↓&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;

      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"col-span-3 flex justify-around mt-8"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-green-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;A&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;A&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-blue-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;B&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;B&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-red-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;X&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;X&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"btn bg-yellow-600"&lt;/span&gt; &lt;span class="na"&gt;onClick&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;player&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sendMove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Y&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Y&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;button&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Live demo &amp;amp; try it yourself
&lt;/h3&gt;

&lt;p&gt;The original project has a nice little demo:&lt;/p&gt;

&lt;p&gt;→ &lt;a href="https://SlaneyEE.github.io/touch-coop/demos/match.html" rel="noopener noreferrer"&gt;https://SlaneyEE.github.io/touch-coop/demos/match.html&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Final thoughts
&lt;/h3&gt;

&lt;p&gt;TouchCoop is a beautiful example of how far browser APIs have come: WebRTC + TypeScript + modern build tools = couch co-op without native apps or complex backends.&lt;/p&gt;

&lt;p&gt;If you're building casual multiplayer experiences or party games give it a try.&lt;/p&gt;

&lt;p&gt;Have you built (or are you planning to build) a couch co-op game? Drop a comment below — I'd love to hear your multiplayer war stories or see links to your projects!&lt;/p&gt;

&lt;p&gt;Happy coding, and see you in the comments ✌️&lt;/p&gt;

</description>
      <category>typescript</category>
      <category>gamedev</category>
      <category>webrtc</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Claude Opus 4.5 changes everything</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Thu, 08 Jan 2026 23:21:22 +0000</pubDate>
      <link>https://forem.com/remojansen/claude-opus-45-changes-everything-4h12</link>
      <guid>https://forem.com/remojansen/claude-opus-45-changes-everything-4h12</guid>
      <description>&lt;h1&gt;
  
  
  Experimenting with AI-Generated Code in 2025
&lt;/h1&gt;

&lt;p&gt;Before I begin, I would like to clarify my position. I'm one of those people who believe that AGI will happen. I don't know when, but I believe that while the human mind and consciousness are extraordinarily complex, they are ultimately governed by the physical laws of the universe and can therefore be simulated. Someday AGI will be a reality. How far are we? I have no idea, and I don't care.&lt;/p&gt;

&lt;p&gt;What I do care about is how the current growing capabilities of LLMs will impact what has been the source of income for my family for the last 15 years. So I have been keeping an eye on emerging tools, workflows, and new approaches to software engineering. For over two years, I have been using GitHub Copilot extensively with multiple models, coding agents, and custom agents, and for the most part it has been hit and miss.&lt;/p&gt;

&lt;p&gt;I usually ask GitHub Copilot to implement a feature or fix a bug using the chat or coding agent, and a lot of times it would go very wrong. I have developed the habit of staging changes before each prompt, code reviewing changes for each prompt as I go along, and rolling back via git when I'm not happy with the solution. Working like this for a while means that I have been able to develop a sense of what kinds of things will work and how to break problems into steps that make it more likely that the AI agent will do what I expect.&lt;/p&gt;

&lt;p&gt;I know some developers feel like AI agents are taking the joy of coding away from them, but I do not feel that way because it has allowed me to spend more time developing features. I feel like finding the root cause of bugs (even when I end up fixing them manually) is one of the things that LLMs can be very good at. As a result, I find myself spending way less time debugging.&lt;/p&gt;

&lt;p&gt;Another thing, in my opinion, that is better is that I have to deal less with "repetitive tasks". After 15 years in the sector, I find that these days I enjoy spending more time trying to understand the business problem and designing a solution than actually implementing it. Once I have designed the API contracts, boundary contexts, database schema, etc., the implementation becomes very much grunt work—something that I feel I have done so many times that it is no longer enjoyable. So in 2025, I spent much more time doing code reviews and technical specs (for the LLMs) and less on implementation.&lt;/p&gt;

&lt;p&gt;Do I feel like I have become more productive in 2025? Not really—maybe a little bit, but not a lot. I would say overall, a task took more or less the same time, but I spent more time thinking about the problem and doing verification than doing implementation.&lt;/p&gt;

&lt;h1&gt;
  
  
  Experiencing Claude Opus 4.5 for the First Time
&lt;/h1&gt;

&lt;p&gt;It was at this point that the Christmas holidays arrived, and I happened to have unlimited Claude Opus 4.5 tokens for a couple of weeks. So I decided to work on some old side projects that I never had time to finish.&lt;/p&gt;

&lt;p&gt;One of the projects was quite obsolete; I had not worked on it for a very long time. It originally used Create React App, but I knew that it had since been deprecated, so the first thing I did was to ask Claude to plan a migration from Create React App to Vite. I was quite impressed by the quality of the plan, so I asked it to go ahead, and 3 minutes later, I had everything working as expected.&lt;/p&gt;

&lt;p&gt;I also had plans to make the web app work as a mobile app via Capacitor and as a desktop app via Electron. The problem is that on each platform I would have to use different native APIs—for example, to store user progress (the app is a game). On the web I use local storage, in Node.js the fs module, and in mobile apps SQLite. I needed Opus to implement an interface and implementations for each platform, then implement the builds for web, Windows, Mac, Linux, Android, and iOS. In about 10-15 minutes I had a working version. I tested it and encountered some small issues. I wanted the app to be full screen, and in Electron it was not using the entire screen, but after one or two prompts everything was done.&lt;/p&gt;

&lt;p&gt;I was already quite impressed because when I reviewed the changes I didn't spot anything too bad. These were complex tasks, and Claude Opus 4.5 was getting them done one prompt at a time. The most impressive part is that I didn't have to explain how I wanted it done. I explained why I needed something and let the planning agent do all the planning for me. Now I only had to spend time doing code reviews, and because the code was fairly good, I was moving very fast.&lt;/p&gt;

&lt;p&gt;I started to ask for features, and I was able to get over 10 features in one morning. I also tried Claude on a project really outside of my comfort zone. I wanted to migrate an OpenGL app to WebGL from Qt to TypeScript. I was able to implement the migration just the way I wanted in half a morning. Suddenly, when you are running 6 AI agents in parallel, it is like freaking horizontally scaling yourself. While the agents implement a set of features, you review the previous set and spend some time thinking about the next set. You feel like you are truly being much more productive.&lt;/p&gt;

&lt;p&gt;The holidays passed, and on the 31st of December at midnight my unlimited tokens came to an end. It is now January, and when I tried to code without it, I felt like the other models seemed dumb in comparison. The joy of developing a product for me is not in coding the product—don't get me wrong, I love coding—but the true joy comes from seeing people enjoying it. With Claude Opus 4.5, I can deliver more products and more features than ever before. I can focus on listening to my users and not have to suffer all the repetitive and tedious stuff. Will the code be as good as NASA's or as beautiful as poetry? No, but it will certainly be good enough to delight users, and for me, that is enough.&lt;/p&gt;

&lt;p&gt;I'm now looking at the higher-tier Claude subscription and thinking to myself that, considering what I have experienced, it is actually reasonable. I think Claude Opus 4.5 changes everything, and it makes me both very excited and very anxious. Excited because now developers will be able to build things that were not possible before because they required too much effort. This is going to be particularly noticeable in open source. We will soon see really powerful open source solutions (that are not just libraries) that can compete with big SaaS players. Anxious because it is hard to see how this is not going to impact job security in the long run.&lt;/p&gt;

&lt;p&gt;Note: What I’m describing above is not vibe coding. I still review every change, work in feature branches, run CI/CD pipelines, and understand the code that ships. This is agent-driven software engineering, not blind prompt-and-pray development.&lt;/p&gt;

&lt;p&gt;Have you tried Opus? What is the most impressive use case you have experienced?&lt;/p&gt;

&lt;p&gt;In my next post I will talk about how I plan to combat my anxious thoughts about my career as a software engineer and focus my energy on the exciting ones.&lt;/p&gt;

</description>
      <category>programming</category>
      <category>ai</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I have open-sourced a WebGL front-end for your terminal that emulates a CRT monitor</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sat, 03 Jan 2026 00:11:53 +0000</pubDate>
      <link>https://forem.com/remojansen/i-have-open-sourced-a-webgl-front-end-for-your-terminal-that-emulates-a-crt-monitor-2icd</link>
      <guid>https://forem.com/remojansen/i-have-open-sourced-a-webgl-front-end-for-your-terminal-that-emulates-a-crt-monitor-2icd</guid>
      <description>&lt;p&gt;I'm thrilled to announce that I've open sourced &lt;strong&gt;cool-retro-term-webgl&lt;/strong&gt;, a modern WebGL-based recreation of the beloved &lt;em&gt;cool-retro-term&lt;/em&gt; terminal emulator!&lt;/p&gt;

&lt;p&gt;For years, developers and retro computing enthusiasts have loved &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;cool-retro-term&lt;/a&gt; by Filippo Scognamiglio (Swordfish90) — a Qt-based terminal that perfectly mimics the look and feel of old cathode ray tube (CRT) monitors, complete with scanlines, glow, and that nostalgic flicker.&lt;/p&gt;

&lt;p&gt;I wanted to bring those authentic retro effects to the web and modern applications. The original is built in QML and C++, so I set out to port the shader magic to &lt;strong&gt;WebGL&lt;/strong&gt;, making it usable in browsers, web apps, and even native desktop apps via Electron.&lt;/p&gt;

&lt;p&gt;The result? A lightweight, high-performance CRT renderer that integrates seamlessly with &lt;strong&gt;XTerm.js&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Authentic retro CRT effects powered by WebGL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Screen curvature and distortion&lt;/li&gt;
&lt;li&gt;Phosphor glow and bloom&lt;/li&gt;
&lt;li&gt;Scanlines and rasterization&lt;/li&gt;
&lt;li&gt;RGB chromatic aberration&lt;/li&gt;
&lt;li&gt;Flicker, static noise, and burn-in persistence&lt;/li&gt;
&lt;li&gt;Horizontal sync jitter&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;Two packages in a monorepo:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cool-retro-term-renderer&lt;/code&gt;&lt;/strong&gt;: The core library for adding CRT effects to any XTerm.js instance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;cool-retro-term-electron&lt;/code&gt;&lt;/strong&gt;: A full-featured desktop terminal app built with Electron, supporting real shell processes via node-pty.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Here's a glimpse of the retro magic in action:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flidyuh2pkr5fth2ojnpe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Flidyuh2pkr5fth2ojnpe.png" alt="Preview" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Live demo &lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Try the Desktop App
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/remojansen/cool-retro-term-webgl/releases" rel="noopener noreferrer"&gt;Download the Mac binary&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The project is licensed under GPL-3.0, just like the original.&lt;/p&gt;

&lt;p&gt;Check out the repo: &lt;a href="https://github.com/remojansen/cool-retro-term-webgl" rel="noopener noreferrer"&gt;https://github.com/remojansen/cool-retro-term-webgl&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Thanks to Swordfish90 for the original inspiration.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>webgl</category>
      <category>opensource</category>
      <category>threejs</category>
    </item>
    <item>
      <title>Building a Retro CRT Terminal Website with WebGL and GitHub Copilot (Claude Opus 4.5)</title>
      <dc:creator>Remo H. Jansen</dc:creator>
      <pubDate>Sun, 28 Dec 2025 02:10:00 +0000</pubDate>
      <link>https://forem.com/remojansen/building-a-retro-crt-terminal-website-with-webgl-and-github-copilot-claude-opus-35-3jfd</link>
      <guid>https://forem.com/remojansen/building-a-retro-crt-terminal-website-with-webgl-and-github-copilot-claude-opus-35-3jfd</guid>
      <description>&lt;p&gt;During the holidays, I was browsing the internet when I came across &lt;a href="https://github.com/Swordfish90/cool-retro-term" rel="noopener noreferrer"&gt;cool-retro-term&lt;/a&gt;, an open-source terminal that mimics the visuals of old cathode ray tube (CRT) displays.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lv0qdo9ixipe6szin5n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8lv0qdo9ixipe6szin5n.png" alt="Terminal preview" width="800" height="663"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I loved the look of it—it has an awesome retro sci-fi atmosphere that reminds me of the Alien and Fallout fictional universes. I thought it would be amazing if I could make my personal website look like that. I took a look at the source code and quickly realized that it's implemented using QML and C++. I'm not experienced with either, so I wondered if it would be possible to port it to web technologies using either WebGL or Emscripten. I asked GitHub Copilot, and it advised me to try the WebGL route because there were fewer technical challenges involved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the Architecture
&lt;/h2&gt;

&lt;p&gt;I have no experience with QML, but I have worked with Three.js in the past. I don't have a deep understanding of how shaders work internally, but with the help of GitHub Copilot, I was able to understand the architecture of the original source code. I cloned the repo, added a new web directory, and started providing instructions to Claude. The original application has two sets of shaders: the first one handles the static frame, and the second one is a series of effects that are influenced by the current time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: The Static Frame
&lt;/h2&gt;

&lt;p&gt;I started by asking Claude to implement the static frame using Three.js while ignoring the terminal emulation for now. This gave me a foundation to build upon without getting overwhelmed by complexity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Text Rendering
&lt;/h2&gt;

&lt;p&gt;The second step was to ask Claude to render some basic text from a hardcoded text file in the Three.js scene using the appropriate retro font.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Migrating the Visual Effects
&lt;/h2&gt;

&lt;p&gt;Then I started to migrate the visual effects—starting with the background noise and then moving on to the other effects:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bloom&lt;/strong&gt; – A glow effect that makes bright areas bleed into surrounding pixels&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Brightness&lt;/strong&gt; – Controls the overall luminosity of the display&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chroma Color&lt;/strong&gt; – Adds color tinting to simulate phosphor characteristics&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;RGB Shift&lt;/strong&gt; – Separates color channels slightly to mimic CRT color misalignment&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Screen Curvature&lt;/strong&gt; – Warps the image to simulate the curved glass of old monitors&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burn-In&lt;/strong&gt; – Simulates phosphor burn-in from static images left on screen too long&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flickering&lt;/strong&gt; – Adds subtle brightness fluctuations like real CRT displays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Glowing Line&lt;/strong&gt; – Renders a scanning beam effect moving across the screen&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal Sync&lt;/strong&gt; – Simulates horizontal sync issues causing image distortion&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Jitter&lt;/strong&gt; – Adds small random movements to simulate signal instability&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rasterization&lt;/strong&gt; – Renders visible scan lines characteristic of CRT displays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static Noise&lt;/strong&gt; – Adds animated noise/grain to the image&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At this point, there were some visual bugs that required a bit of trial and error until the LLM was able to fix them without introducing new issues. The main one was a problem related to the position of the screen reflections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Integrating Xterm.js
&lt;/h2&gt;

&lt;p&gt;Once I was able to get the terminal frame and the effects ported from OpenGL to WebGL, I asked the LLM to replace the hardcoded text with the output of &lt;a href="https://xtermjs.org/" rel="noopener noreferrer"&gt;Xterm.js&lt;/a&gt;. Xterm.js is an open-source project designed to be a web-based front-end for terminals. It's used in tools like Visual Studio Code because VS Code is a web application that runs inside Electron—Xterm.js is the front-end within VS Code that accesses a real terminal instance on your machine.&lt;/p&gt;

&lt;p&gt;In my case, I don't need a real terminal, so I asked Claude to create a terminal emulator with a bunch of basic commands such as &lt;code&gt;clear&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cd&lt;/code&gt;, and &lt;code&gt;cat&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falndqf12m1dp7cy4kvzf.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Falndqf12m1dp7cy4kvzf.png" alt="Terminal LS" width="800" height="603"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Building Games
&lt;/h2&gt;

&lt;p&gt;At this point, everything was almost complete, so I asked Claude to implement multiple text-based games. I implemented games like Pong, Tetris, Snake, Minesweeper, Space Invaders, and Arkanoid—and most of them worked almost perfectly on the first attempt. Some of the games experienced minor visual issues, but I was able to solve everything by describing the issue in detail to Claude and what I thought was the root cause.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzn5cvio687tammhtg85.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffzn5cvio687tammhtg85.png" alt="Terminal Game" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4r9cgb4j4uicteahnlv2.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4r9cgb4j4uicteahnlv2.png" alt="Terminal Game" width="800" height="609"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhlcmeakqokwq1q7wpuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fbhlcmeakqokwq1q7wpuz.png" alt="Terminal Game" width="800" height="606"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh2c2tkwgefc6yaoyrym.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Feh2c2tkwgefc6yaoyrym.png" alt="Terminal Game" width="800" height="604"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Adding Media Playback with ffplay
&lt;/h2&gt;

&lt;p&gt;I also wanted to add support for playing audio and video files directly in the terminal, similar to how &lt;code&gt;ffplay&lt;/code&gt; works in a real terminal. I asked Claude to implement an &lt;code&gt;ffplay&lt;/code&gt; command that could render video with all the effects previously implemented.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr87itknku6zpgjugeyf9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fr87itknku6zpgjugeyf9.png" alt="Terminal ffplay" width="800" height="600"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Refactoring and Publishing
&lt;/h2&gt;

&lt;p&gt;The final step was to ask Claude to refactor the code to clearly separate the library code (the WebGL retro terminal renderer) from my application code (the terminal emulator and games). The goal was to publish the WebGL terminal renderer as a standalone npm module, and Claude was able to do it with zero issues in just one attempt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Overall, the entire implementation took around 10–15 hours. Without an LLM, it would have taken me several weeks. I think this project has been an interesting way to demonstrate how powerful LLMs can be as development tools—especially when working with unfamiliar technologies like shader programming.&lt;/p&gt;

&lt;p&gt;By the end of the experiment, I consumed about 50% of my monthly GitHub Copilot for Business tokens ($21/month), which means the entire project cost me roughly $10.50. When you consider that this would have taken weeks of work otherwise, the cost savings enabled by Claude Opus are absolutely insane.&lt;/p&gt;

&lt;p&gt;If you're curious, you can check out the result at &lt;a href="https://remojansen.github.io/" rel="noopener noreferrer"&gt;https://remojansen.github.io/&lt;/a&gt; or browse the source code on &lt;a href="https://github.com/remojansen/remojansen.github.io" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>opensource</category>
      <category>webdev</category>
      <category>webgl</category>
    </item>
  </channel>
</rss>
