diff --git a/src/tg/builder/RenderPassBuilder.hh b/src/tg/builder/RenderPassBuilder.hh
index 8214b04340e8b9545057897ed39fe57aa6c2e552..af9da2023237f7847699fb60cea94cc6d2e6fee1 100644
--- a/src/tg/builder/RenderPassBuilder.hh
+++ b/src/tg/builder/RenderPassBuilder.hh
@@ -5,6 +5,7 @@
 
 #include <tg/typed-graphics-lean.hh>
 
+#include <tg/data/attachment.hh>
 #include <tg/detail/traits.hh>
 #include <tg/objects/RenderPass.hh>
 
@@ -23,16 +24,29 @@ struct SubPassBuilder
         attachments.emplace_back(attachment_action::write, type_of<T>, std::move(name));
     }
     template <class T>
+    void write(attachment<T> const& attachment)
+    {
+        // TODO: store attachment
+        attachments.emplace_back(attachment_action::write, type_of<T>, attachment.name);
+    }
+    template <class T>
     void read(std::string name)
     {
         attachments.emplace_back(attachment_action::read, type_of<T>, std::move(name));
     }
+    template <class T>
+    void read(attachment<T> const& attachment)
+    {
+        // TODO: store attachment
+        attachments.emplace_back(attachment_action::read, type_of<T>, attachment.name);
+    }
 
-    template <class VertexT>
-    PrimitivePipelineBuilder buildPipeline()
+    template <class VertexT, class FragmentT>
+    PrimitivePipelineBuilder<VertexT, FragmentT> buildPipeline(SharedVertexShader<VertexT> const& vs,
+                                                               SharedFragmentShader<FragmentT> const& fs,
+                                                               topology topo = topology::triangles)
     {
-        // TODO: vertex type
-        return {};
+        return {vs, fs, topo};
     }
 
 private:
@@ -43,16 +57,16 @@ private:
     };
 
     // closely mirrors https://www.khronos.org/registry/vulkan/specs/1.1-extensions/man/html/VkAttachmentDescription.html
-    struct attachment
+    struct rp_attachment
     {
         attachment_action action;
         type format;
         std::string name;
 
-        attachment() = default;
-        attachment(attachment_action a, type f, std::string n) : action(a), format(f), name(std::move(n)) {}
+        rp_attachment() = default;
+        rp_attachment(attachment_action a, type f, std::string n) : action(a), format(f), name(std::move(n)) {}
     };
-    std::vector<attachment> attachments;
+    std::vector<rp_attachment> attachments;
 
     friend class Device;
 };
@@ -64,6 +78,12 @@ struct RenderPassBuilder
     {
         clearInfos.push_back(clear_info(std::move(name), clearValue));
     }
+    template <class T>
+    void clear(attachment<T> const& attachment, T const& clearValue)
+    {
+        // TODO: store attachment instead
+        clearInfos.push_back(clear_info(attachment.name, clearValue));
+    }
 
     void clearDepth(std::string name, f32 clearDepth, u32 clearStencil = 0)
     {
diff --git a/src/tg/builder/pipelines/PrimitivePipelineBuilder.hh b/src/tg/builder/pipelines/PrimitivePipelineBuilder.hh
index 55a203e88abdcb5504bfd31fb42caf5a0ec855fc..ce547b560c0fcde92a69c322088aa5850faff23c 100644
--- a/src/tg/builder/pipelines/PrimitivePipelineBuilder.hh
+++ b/src/tg/builder/pipelines/PrimitivePipelineBuilder.hh
@@ -8,28 +8,35 @@
 
 namespace tg
 {
-// TODO: template to vertex type
+template <class VertexT, class FragmentT>
 struct PrimitivePipelineBuilder
 {
-    void vertexShader(SharedVertexShader const& shader, topology topology = topology::triangles)
+    PrimitivePipelineBuilder(SharedVertexShader<VertexT> const& vs, SharedFragmentShader<FragmentT> const& fs, topology topology = topology::triangles)
     {
         // TODO
     }
-    void tessellation(int patchSize, SharedTessellationControlShader const& tecShader, SharedTessellationEvaluationShader const& tevShader)
+
+    PrimitivePipelineBuilder& tessellation(int patchSize, SharedTessellationControlShader const& tecShader, SharedTessellationEvaluationShader const& tevShader)
     {
         // TODO
+        return *this;
     }
-    void geometryShader(SharedGeometryShader const& shader)
+    PrimitivePipelineBuilder& geometryShader(SharedGeometryShader const& shader)
     {
         // TODO
+        return *this;
     }
-    void fragmentShader(SharedFragmentShader const& shader)
+
+    PrimitivePipelineBuilder& cullMode(cull_mode cm)
     {
-        // TODO
+        cull_mode = cm;
+        return *this;
+    }
+    PrimitivePipelineBuilder& polygonMode(polygon_mode pm)
+    {
+        polygon_mode = pm;
+        return *this;
     }
-
-    void cullMode(cull_mode cm) { cull_mode = cm; }
-    void polygonMode(polygon_mode pm) { polygon_mode = pm; }
 
 private:
     cull_mode cull_mode = cull_mode::none;
diff --git a/src/tg/common/macros.hh b/src/tg/common/macros.hh
index 4be202c1e6213f2ac984ce58e5df2c92f43e4a0c..31d57a470fdd99f6d63d317c696ae86bf7291919 100644
--- a/src/tg/common/macros.hh
+++ b/src/tg/common/macros.hh
@@ -7,6 +7,16 @@
 #define TG_SHARED(klass, type) \
     klass type;                \
     using Shared##type = std::shared_ptr<type> // force ;
+#define TG_SHARED_T1(klass, type, T1) \
+    template <class T1>               \
+    klass type;                       \
+    template <class T1>               \
+    using Shared##type = std::shared_ptr<type<T1>> // force ;
+#define TG_SHARED_T2(klass, type, T1, T2) \
+    template <class T1, class T2>         \
+    klass type;                           \
+    template <class T1, class T2>         \
+    using Shared##type = std::shared_ptr<type<T1, T2>> // force ;
 
 // unified interface for builders
 // #define TG_BUILD(type)                                       \
diff --git a/src/tg/data/attachment.hh b/src/tg/data/attachment.hh
new file mode 100644
index 0000000000000000000000000000000000000000..248e499d2d1d91cbab86d153bed41042e6907d81
--- /dev/null
+++ b/src/tg/data/attachment.hh
@@ -0,0 +1,21 @@
+#pragma once
+
+#include <tg/typed-graphics-lean.hh>
+
+namespace tg
+{
+template <class T>
+struct attachment
+{
+    std::string name;
+    // TODO: image
+
+    attachment(SharedImage2D<T> const& img, std::string name) : name(std::move(name)) {}
+};
+
+template <class T>
+attachment<T> make_attachment(SharedImage2D<T> const& img, std::string name)
+{
+    return {img, std::move(name)};
+}
+} // namespace tg
\ No newline at end of file
diff --git a/src/tg/objects/CommandBuffer.hh b/src/tg/objects/CommandBuffer.hh
index 1b04b94e0361996fa47e6ff94132fe4910b9cc23..6c0aa04bc20a86654da75c4534fccda32e19ff94 100644
--- a/src/tg/objects/CommandBuffer.hh
+++ b/src/tg/objects/CommandBuffer.hh
@@ -1,6 +1,7 @@
 #pragma once
 
 #include <tg/typed-geometry-lean.hh>
+#include <tg/typed-graphics-lean.hh>
 
 namespace tg
 {
@@ -18,8 +19,42 @@ public:
     CommandBuffer() = default;
 
 public:
-    struct Recorder {
+    template <class VertexT, class FragmentT>
+    struct PrimitivePipelineRecorder
+    {
+        // TODO: record current vb/ib for rebinding
+
+        void bindResources()
+        {
+            // TODO
+        }
+
+        void draw(SharedBuffer<VertexT> vertexBuffer)
+        {
+            // TODO
+        }
+
+        template <class PushConstants>
+        void draw(PushConstants const& constants, SharedBuffer<VertexT> vertexBuffer)
+        {
+            // TODO
+        }
+    };
+    struct RenderPassRecorder
+    {
+        template <class VertexT, class FragmentT>
+        PrimitivePipelineRecorder<VertexT, FragmentT> recordPipeline(SharedPrimitivePipeline<VertexT, FragmentT> const& pipeline)
+        {
+            return {};
+        }
+    };
+    struct Recorder
+    {
         // TODO
+
+        RenderPassRecorder recordPass(SharedRenderPass const& pass) { return {}; }
+        RenderPassRecorder recordPass(SharedRenderPass const& pass, ibox2 viewport) { return {}; }
+        RenderPassRecorder recordPass(SharedRenderPass const& pass, ibox2 viewport, ibox2 scissor) { return {}; }
     };
 };
 } // namespace tg
diff --git a/src/tg/objects/Device.cc b/src/tg/objects/Device.cc
index f43b7144a3535363a473e507a2252e1115c43060..b392645fa02771438d66b5514635036c4d20a349 100644
--- a/src/tg/objects/Device.cc
+++ b/src/tg/objects/Device.cc
@@ -25,13 +25,6 @@ SharedRenderPass Device::createRenderPass(SubPassBuilder const& builder)
     return std::make_shared<RenderPass>();
 }
 
-SharedPrimitivePipeline Device::createPrimitivePipeline(PrimitivePipelineBuilder const& builder)
-{
-    // TODO
-    (void)builder;
-    return std::make_shared<PrimitivePipeline>();
-}
-
 SharedCommandBuffer Device::createCommandBuffer()
 {
     // TODO
diff --git a/src/tg/objects/Device.hh b/src/tg/objects/Device.hh
index 773d92df8f880ebe3a6144986c82dd9e477209e2..84e337f1581fb69745e1dc2055668a7f1e93d687 100644
--- a/src/tg/objects/Device.hh
+++ b/src/tg/objects/Device.hh
@@ -12,6 +12,8 @@
 
 #include "CommandBuffer.hh"
 
+#include "pipelines/PrimitivePipeline.hh"
+
 namespace tg
 {
 class Device : std::enable_shared_from_this<Device>
@@ -54,7 +56,8 @@ public:
     SharedRenderPass createRenderPass(RenderPassBuilder const& builder);
     SharedRenderPass createRenderPass(SubPassBuilder const& builder);
 
-    SharedPrimitivePipeline createPrimitivePipeline(PrimitivePipelineBuilder const& builder);
+    template <class VertexT, class FragmentT>
+    SharedPrimitivePipeline<VertexT, FragmentT> createPrimitivePipeline(PrimitivePipelineBuilder<VertexT, FragmentT> const& builder);
 
     // commands
 public:
@@ -71,4 +74,11 @@ public:
 public:
     static SharedDevice create();
 };
+
+template <class VertexT, class FragmentT>
+SharedPrimitivePipeline<VertexT, FragmentT> Device::createPrimitivePipeline(PrimitivePipelineBuilder<VertexT, FragmentT> const& builder)
+{
+    // TODO
+    return std::make_shared<PrimitivePipeline<VertexT, FragmentT>>();
+}
 } // namespace tg
diff --git a/src/tg/objects/Window.hh b/src/tg/objects/Window.hh
index 8d91f3230ebf0fd1f16c92ed34405bfcdc40a9e3..c19c96fbb5f74bb4c5f1bdefe7e1f8a77dc96cf7 100644
--- a/src/tg/objects/Window.hh
+++ b/src/tg/objects/Window.hh
@@ -14,6 +14,12 @@ public:
 public:
     struct Frame
     {
+        template <class FragmentT>
+        void present(SharedImage2D<FragmentT> const& img)
+        {
+            // TODO
+        }
+
         // TODO: RAII
 
         operator bool() const
diff --git a/src/tg/objects/pipelines/PrimitivePipeline.hh b/src/tg/objects/pipelines/PrimitivePipeline.hh
index 07f4bd251622ec90b7a6c55152c8d75e7108b9dd..b00ec72939dd896979f318b9d4d1436d9ab4f7a6 100644
--- a/src/tg/objects/pipelines/PrimitivePipeline.hh
+++ b/src/tg/objects/pipelines/PrimitivePipeline.hh
@@ -18,22 +18,25 @@ namespace tg
  *	* Depth/Stencil testing
  *	* DescriptorSet layouts
  *	* PushConstant ranges
- *	
+ *
  * The pipeline is specific to a SubPass
- *	
+ *
  * Some state can be configured to be set dynamically:
  *	* Viewport, Scissor, Line Width, Stencils
  *	* Depth Bias, Depth Bounds
  *	* Blend Constants
- *	
+ *
  * TODO:
  *	* for now, viewport and scissor is always dynamic
  */
+template <class VertexT, class FragmentT>
 class PrimitivePipeline
 {
     TG_REFERENCE_TYPE(PrimitivePipeline);
 
 public:
+    using vertex_t = VertexT;
+
     PrimitivePipeline() = default;
 };
 } // namespace tg
\ No newline at end of file
diff --git a/src/tg/typed-graphics-lean.hh b/src/tg/typed-graphics-lean.hh
index f9ae67b503f9326b2f234d37db47449dc7f762f9..2cca20de2fd1e8973a5aabab56139bb0d0357d48 100644
--- a/src/tg/typed-graphics-lean.hh
+++ b/src/tg/typed-graphics-lean.hh
@@ -14,14 +14,17 @@ TG_SHARED(class, Window);
 
 TG_SHARED(class, CommandBuffer);
 
-TG_SHARED(class, PrimitivePipeline);
 TG_SHARED(class, RenderPass);
+TG_SHARED_T2(class, PrimitivePipeline, VertexT, FragmentT);
+TG_SHARED_T1(class, Framebuffer, FragmentT);
 
-TG_SHARED(class, VertexShader);
+TG_SHARED_T1(class, Buffer, DataT);
+
+TG_SHARED_T1(class, VertexShader, VertexT);
 TG_SHARED(class, TessellationControlShader);
 TG_SHARED(class, TessellationEvaluationShader);
 TG_SHARED(class, GeometryShader);
-TG_SHARED(class, FragmentShader);
+TG_SHARED_T1(class, FragmentShader, FragmentT);
 TG_SHARED(class, MeshShader);
 TG_SHARED(class, TaskShader);
 TG_SHARED(class, RayGenerationShader);
@@ -30,11 +33,6 @@ TG_SHARED(class, RayAnyHitShader);
 TG_SHARED(class, RayIntersectionShader);
 TG_SHARED(class, RayMissShader);
 
-template <class FragmentT>
-class Framebuffer;
-template <class FragmentT>
-using SharedFramebuffer = std::shared_ptr<Framebuffer<FragmentT>>;
-
 template <class VertexT, class FragmentT>
 class Shader;
 template <class VertexT, class FragmentT>
@@ -56,9 +54,4 @@ template <class DataT>
 using SharedImage2D = std::shared_ptr<Image2D<DataT>>;
 template <class DataT>
 using SharedImage3D = std::shared_ptr<Image3D<DataT>>;
-
-template <class DataT>
-class Buffer;
-template <class DataT>
-using SharedBuffer = std::shared_ptr<Buffer<DataT>>;
 } // namespace tg
diff --git a/src/tg/typed-graphics.hh b/src/tg/typed-graphics.hh
index ef94402a8ae824e84b2b9439fdd300df473383a9..2cbed002827d8a2e7444b82765d3825cdb85701b 100644
--- a/src/tg/typed-graphics.hh
+++ b/src/tg/typed-graphics.hh
@@ -8,6 +8,14 @@
 #include "common/trace.hh"
 #include "common/traits.hh"
 
+// Data
+#include "data/attachment.hh"
+#include "data/cull_mode.hh"
+#include "data/polygon_mode.hh"
+#include "data/topology.hh"
+#include "data/type.hh"
+#include "data/vertex_format.hh"
+
 // Builder
 #include "builder/builder.hh"