diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
index 624b03125d78b9b7a1591d5830d8281979549f73..a4d2a2a79102b21fce504411bf473db54b351aa2 100644
--- a/src/CMakeLists.txt
+++ b/src/CMakeLists.txt
@@ -1,4 +1,5 @@
 target_sources(${PROJECT_NAME} PRIVATE
+        GraphicsPipeline.cpp
         main.cpp
         Renderer.cpp
         Swapchain.cpp
diff --git a/src/GraphicsPipeline.cpp b/src/GraphicsPipeline.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..6fd8f68bc661ed532b0afcb290150c5cfa8aba65
--- /dev/null
+++ b/src/GraphicsPipeline.cpp
@@ -0,0 +1,128 @@
+#include "GraphicsPipeline.hpp"
+
+[[nodiscard]]
+static auto
+    load_shader_module(const vk::Device t_device, const std::filesystem::path& t_filepath)
+        -> vk::UniqueShaderModule
+{
+    std::ifstream file{ t_filepath, std::ios::binary | std::ios::in | std::ios::ate };
+
+    const std::streamsize file_size = file.tellg();
+    if (file_size == -1) {
+        throw std::runtime_error{
+            std::format("Failed to open file: {}", t_filepath.generic_string())
+        };
+    }
+
+    std::vector<char> buffer(static_cast<size_t>(file_size));
+
+    file.seekg(0, std::ios::beg);
+    file.read(buffer.data(), file_size);
+    file.close();
+
+    const vk::ShaderModuleCreateInfo create_info{
+        .codeSize = static_cast<size_t>(file_size),
+        .pCode    = reinterpret_cast<uint32_t*>(buffer.data())
+    };
+
+    return t_device.createShaderModuleUnique(create_info);
+}
+
+auto create_pipeline_layout(const vk::Device device) -> vk::UniquePipelineLayout
+{
+    constexpr static vk::PipelineLayoutCreateInfo create_info{};
+
+    return device.createPipelineLayoutUnique(create_info);
+}
+
+auto build_graphics_pipeline(
+    const vk::Device         device,
+    const vk::PipelineLayout layout,
+    const vk::RenderPass     render_pass
+) -> vk::UniquePipeline
+{
+    const vk::UniqueShaderModule vertex_shader{
+        load_shader_module(device, "shaders/triangle.frag")
+    };
+    const vk::UniqueShaderModule fragment_shader{
+        load_shader_module(device, "shaders/triangle.frag")
+    };
+    const std::array shader_stages{
+        vk::PipelineShaderStageCreateInfo{   .stage  = vk::ShaderStageFlagBits::eVertex,
+                                          .module = vertex_shader.get(),
+                                          .pName  = "main" },
+        vk::PipelineShaderStageCreateInfo{ .stage  = vk::ShaderStageFlagBits::eFragment,
+                                          .module = fragment_shader.get(),
+                                          .pName  = "main" }
+    };
+
+    constexpr static vk::PipelineVertexInputStateCreateInfo
+        vertex_input_state_create_info{};
+
+    constexpr static vk::PipelineInputAssemblyStateCreateInfo
+        input_assembly_state_create_info{
+            .topology = vk::PrimitiveTopology::eTriangleList,
+        };
+
+    constexpr static vk::PipelineViewportStateCreateInfo viewport_state_create_info{
+        .viewportCount = 1,
+        .scissorCount  = 1,
+    };
+
+    constexpr static vk::PipelineRasterizationStateCreateInfo
+        rasterization_state_create_info{
+            .polygonMode = vk::PolygonMode::eFill,
+            .cullMode    = vk::CullModeFlagBits::eBack,
+            .frontFace   = vk::FrontFace::eCounterClockwise,
+            .lineWidth   = 1.f,
+        };
+
+    constexpr static vk::PipelineMultisampleStateCreateInfo multisample_state_create_info{
+        .rasterizationSamples = vk::SampleCountFlagBits::e1,
+    };
+
+    constexpr static vk::PipelineDepthStencilStateCreateInfo
+        depth_stencil_state_create_info{
+            .depthTestEnable       = true,
+            .depthWriteEnable      = true,
+            .depthCompareOp        = vk::CompareOp::eLess,
+            .depthBoundsTestEnable = false,
+            .stencilTestEnable     = false,
+        };
+
+    constexpr static vk::PipelineColorBlendAttachmentState color_blend_attachment_state{
+        .blendEnable    = vk::False,
+        .colorWriteMask = vk::ColorComponentFlagBits::eR | vk::ColorComponentFlagBits::eG
+                        | vk::ColorComponentFlagBits::eB | vk::ColorComponentFlagBits::eA,
+    };
+    constexpr static vk::PipelineColorBlendStateCreateInfo color_blend_state_create_info{
+        .attachmentCount = 1,
+        .pAttachments    = &color_blend_attachment_state,
+    };
+
+    constexpr static std::array dynamic_states{
+        vk::DynamicState::eViewport,
+        vk::DynamicState::eScissor,
+    };
+    constexpr static vk::PipelineDynamicStateCreateInfo dynamic_state_create_info{
+        .dynamicStateCount = static_cast<uint32_t>(dynamic_states.size()),
+        .pDynamicStates    = dynamic_states.data(),
+    };
+
+    const vk::GraphicsPipelineCreateInfo create_info{
+        .stageCount          = static_cast<uint32_t>(shader_stages.size()),
+        .pStages             = shader_stages.data(),
+        .pVertexInputState   = &vertex_input_state_create_info,
+        .pInputAssemblyState = &input_assembly_state_create_info,
+        .pViewportState      = &viewport_state_create_info,
+        .pRasterizationState = &rasterization_state_create_info,
+        .pMultisampleState   = &multisample_state_create_info,
+        .pDepthStencilState  = &depth_stencil_state_create_info,
+        .pColorBlendState    = &color_blend_state_create_info,
+        .pDynamicState       = &dynamic_state_create_info,
+        .layout              = layout,
+        .renderPass          = render_pass,
+    };
+
+    return device.createGraphicsPipelineUnique(nullptr, create_info).value;
+}
diff --git a/src/GraphicsPipeline.hpp b/src/GraphicsPipeline.hpp
new file mode 100644
index 0000000000000000000000000000000000000000..9e33271de6486d29c98ed5bb90423cb43a427e72
--- /dev/null
+++ b/src/GraphicsPipeline.hpp
@@ -0,0 +1,13 @@
+#pragma once
+
+#include <vulkan/vulkan.hpp>
+
+[[nodiscard]]
+auto create_pipeline_layout(vk::Device) -> vk::UniquePipelineLayout;
+
+[[nodiscard]]
+auto build_graphics_pipeline(
+    vk::Device         device,
+    vk::PipelineLayout layout,
+    vk::RenderPass     render_pass
+) -> vk::UniquePipeline;
diff --git a/src/Renderer.cpp b/src/Renderer.cpp
index 9b34c28d3a3697541e2cf8ba41023272a8bbe782..9896107d41a0be9b08364f9b5be02a85d61c3aca 100644
--- a/src/Renderer.cpp
+++ b/src/Renderer.cpp
@@ -135,6 +135,57 @@ static auto
     return physical_device.createDeviceUnique(device_create_info);
 }
 
+[[nodiscard]]
+static auto create_render_pass(const vk::Format color_format, const vk::Device device)
+    -> vk::UniqueRenderPass
+{
+    const vk::AttachmentDescription color_attachment_description{
+        .format         = color_format,
+        .samples        = vk::SampleCountFlagBits::e1,
+        .loadOp         = vk::AttachmentLoadOp::eClear,
+        .storeOp        = vk::AttachmentStoreOp::eStore,
+        .stencilLoadOp  = vk::AttachmentLoadOp::eDontCare,
+        .stencilStoreOp = vk::AttachmentStoreOp::eDontCare,
+        .initialLayout  = vk::ImageLayout::eUndefined,
+        .finalLayout    = vk::ImageLayout::ePresentSrcKHR,
+    };
+
+    const std::array attachment_descriptions{
+        color_attachment_description,
+    };
+
+    constexpr static vk::AttachmentReference color_attachment_reference{
+        .attachment = 0,
+        .layout     = vk::ImageLayout::eColorAttachmentOptimal,
+    };
+
+    const vk::SubpassDescription subpass_description{
+        .pipelineBindPoint    = vk::PipelineBindPoint::eGraphics,
+        .colorAttachmentCount = 1,
+        .pColorAttachments    = &color_attachment_reference,
+    };
+
+    constexpr static vk::SubpassDependency subpass_dependency{
+        .srcSubpass    = VK_SUBPASS_EXTERNAL,
+        .dstSubpass    = 0,
+        .srcStageMask  = vk::PipelineStageFlagBits::eColorAttachmentOutput,
+        .dstStageMask  = vk::PipelineStageFlagBits::eColorAttachmentOutput,
+        .srcAccessMask = vk::AccessFlagBits::eNone,
+        .dstAccessMask = vk::AccessFlagBits::eColorAttachmentWrite,
+    };
+
+    const vk::RenderPassCreateInfo render_pass_create_info{
+        .attachmentCount = static_cast<uint32_t>(attachment_descriptions.size()),
+        .pAttachments    = attachment_descriptions.data(),
+        .subpassCount    = 1,
+        .pSubpasses      = &subpass_description,
+        .dependencyCount = 1,
+        .pDependencies   = &subpass_dependency,
+    };
+
+    return device.createRenderPassUnique(render_pass_create_info);
+}
+
 Renderer::Renderer(const Window& window)
     : m_instance{ create_instance() },
       m_surface{ window.create_vulkan_surface(m_instance.get()) },
@@ -150,5 +201,6 @@ Renderer::Renderer(const Window& window)
           m_queue_family_index,
           m_device.get(),
           window.framebuffer_size()
-      ) }
+      ) },
+      m_render_pass{ create_render_pass(m_swapchain->format(), m_device.get()) }
 {}
diff --git a/src/Renderer.hpp b/src/Renderer.hpp
index 3f4c0c104b2af50f35851414f85f55b7c942dcee..7d2cc0740804ab97b816f1d20aef673932ec4783 100644
--- a/src/Renderer.hpp
+++ b/src/Renderer.hpp
@@ -12,13 +12,15 @@ public:
     explicit Renderer(const Window& window);
 
 private:
-    vk::UniqueInstance m_instance;
+    vk::UniqueInstance   m_instance;
     vk::UniqueSurfaceKHR m_surface;
-    
+
     vk::PhysicalDevice m_physical_device;
     uint32_t           m_queue_family_index;
     vk::UniqueDevice   m_device;
     vk::Queue          m_queue;
 
     std::optional<Swapchain> m_swapchain;
+
+    vk::UniqueRenderPass m_render_pass;
 };