summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorgrothedev <grothedev@gmail.com>2025-10-02 01:27:50 -0400
committergrothedev <grothedev@gmail.com>2025-10-02 01:27:50 -0400
commita162c98ce54159e3e7dbe867d908ce3276b7f633 (patch)
treeef49d87f7c5345e8ed08884da99a35af7b3c2b5b
parent685ab63782daeb868a999ec65ef0ceb0883acb3e (diff)
trying c++ impl
-rw-r--r--.gitignore1
-rw-r--r--AGENTS.md10
-rw-r--r--cpp-timeplot/CMakeLists.txt87
-rw-r--r--cpp-timeplot/README.md32
-rw-r--r--cpp-timeplot/shaders/waterfall.wgsl22
-rw-r--r--cpp-timeplot/src/main.cpp143
-rw-r--r--cpp-timeplot/src/renderer.cpp180
-rw-r--r--cpp-timeplot/src/renderer.h35
-rw-r--r--cpp-timeplot/src/waterfall.cpp164
-rw-r--r--cpp-timeplot/src/waterfall.h39
-rw-r--r--cpp-timeplot/src/webgpu_impl.cpp2
11 files changed, 715 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index ea8c4bf..fd85fde 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1 +1,2 @@
/target
+/cpp-timeplot/build/
diff --git a/AGENTS.md b/AGENTS.md
index 96e4bff..0ceee95 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -40,3 +40,13 @@ there shall be a state management system similar to js react, whereby UI element
### GUI details
- TODO
+
+
+
+## Next Steps
+- Rust
+ - split code into modules
+ - fix text label title
+ - add plot labels and legends
+- C++
+ - initial implementation
diff --git a/cpp-timeplot/CMakeLists.txt b/cpp-timeplot/CMakeLists.txt
new file mode 100644
index 0000000..58993dd
--- /dev/null
+++ b/cpp-timeplot/CMakeLists.txt
@@ -0,0 +1,87 @@
+cmake_minimum_required(VERSION 3.20)
+project(timeplot-cpp VERSION 0.1.0)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+# Find dependencies
+find_package(PkgConfig REQUIRED)
+
+# GLFW for windowing
+pkg_check_modules(GLFW REQUIRED glfw3)
+
+# Use precompiled Dawn from Chromium
+option(USE_SYSTEM_DAWN "Use system-installed Dawn" OFF)
+
+if(USE_SYSTEM_DAWN)
+ find_package(dawn REQUIRED)
+else()
+ # Download precompiled Dawn binaries
+ message(STATUS "Downloading precompiled Dawn...")
+
+ set(DAWN_VERSION "6536")
+ set(DAWN_DIR "${CMAKE_BINARY_DIR}/dawn-prebuilt")
+
+ if(NOT EXISTS "${DAWN_DIR}")
+ # Try to download from a prebuilt source
+ # Note: Dawn doesn't officially provide prebuilt binaries, so we'll build once and cache
+ # For now, let's use webgpu-distribution which provides prebuilt headers
+ include(FetchContent)
+
+ # Fetch webgpu-distribution (lighter than full Dawn)
+ FetchContent_Declare(
+ webgpu-distribution
+ GIT_REPOSITORY https://github.com/eliemichel/WebGPU-distribution
+ GIT_TAG main
+ GIT_SHALLOW TRUE
+ )
+
+ FetchContent_MakeAvailable(webgpu-distribution)
+
+ set(WEBGPU_BACKEND "WGPU" CACHE STRING "Backend to use")
+
+ # Fetch webgpu-hpp (C++ wrapper)
+ FetchContent_Declare(
+ webgpu-hpp
+ GIT_REPOSITORY https://github.com/eliemichel/WebGPU-Cpp
+ GIT_TAG main
+ GIT_SHALLOW TRUE
+ )
+ FetchContent_MakeAvailable(webgpu-hpp)
+
+ # Fetch glfw3webgpu (GLFW-WebGPU integration)
+ FetchContent_Declare(
+ glfw3webgpu
+ GIT_REPOSITORY https://github.com/eliemichel/glfw3webgpu
+ GIT_TAG main
+ GIT_SHALLOW TRUE
+ )
+ FetchContent_MakeAvailable(glfw3webgpu)
+ endif()
+endif()
+
+# Source files
+set(SOURCES
+ src/main.cpp
+ src/renderer.cpp
+ src/waterfall.cpp
+ src/webgpu_impl.cpp
+)
+
+add_executable(timeplot ${SOURCES})
+
+target_include_directories(timeplot PRIVATE
+ ${CMAKE_CURRENT_SOURCE_DIR}/src
+ ${GLFW_INCLUDE_DIRS}
+)
+
+target_link_libraries(timeplot PRIVATE
+ ${GLFW_LIBRARIES}
+ webgpu
+ glfw3webgpu
+)
+
+# Copy shaders to build directory
+file(GLOB SHADERS "${CMAKE_CURRENT_SOURCE_DIR}/shaders/*.wgsl")
+file(COPY ${SHADERS} DESTINATION ${CMAKE_BINARY_DIR}/shaders)
diff --git a/cpp-timeplot/README.md b/cpp-timeplot/README.md
new file mode 100644
index 0000000..2700f9f
--- /dev/null
+++ b/cpp-timeplot/README.md
@@ -0,0 +1,32 @@
+# TimePlot C++ Implementation
+
+WebGPU-based waterfall plot renderer using Dawn and GLFW.
+
+## Dependencies
+
+- CMake 3.20+
+- C++20 compiler
+- GLFW3
+- Dawn (fetched automatically by CMake)
+
+## Build
+
+```bash
+mkdir build && cd build
+cmake ..
+make -j$(nproc)
+./timeplot
+```
+
+## Controls
+
+- `G` - Toggle grid visibility
+- `ESC` - Exit application
+
+## Features
+
+- Multi-threaded GPU rendering via Vulkan/WebGPU
+- Multiple graph views (side-by-side)
+- Animated waterfall plots
+- Grid lines and borders
+- Real-time data visualization
diff --git a/cpp-timeplot/shaders/waterfall.wgsl b/cpp-timeplot/shaders/waterfall.wgsl
new file mode 100644
index 0000000..6655301
--- /dev/null
+++ b/cpp-timeplot/shaders/waterfall.wgsl
@@ -0,0 +1,22 @@
+struct VertexInput {
+ @location(0) position: vec2<f32>,
+ @location(1) color: vec3<f32>,
+}
+
+struct VertexOutput {
+ @builtin(position) clip_position: vec4<f32>,
+ @location(0) color: vec3<f32>,
+}
+
+@vertex
+fn vs_main(in: VertexInput) -> VertexOutput {
+ var out: VertexOutput;
+ out.clip_position = vec4<f32>(in.position, 0.0, 1.0);
+ out.color = in.color;
+ return out;
+}
+
+@fragment
+fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
+ return vec4<f32>(in.color, 1.0);
+}
diff --git a/cpp-timeplot/src/main.cpp b/cpp-timeplot/src/main.cpp
new file mode 100644
index 0000000..7afd3e8
--- /dev/null
+++ b/cpp-timeplot/src/main.cpp
@@ -0,0 +1,143 @@
+#include <GLFW/glfw3.h>
+#include <webgpu/webgpu.hpp>
+#include <glfw3webgpu.h>
+#include <iostream>
+#include <memory>
+#include <cstdlib>
+
+#include "renderer.h"
+
+constexpr int WINDOW_WIDTH = 1280;
+constexpr int WINDOW_HEIGHT = 720;
+
+class Application {
+public:
+ Application() : window_(nullptr) {}
+
+ ~Application() {
+ cleanup();
+ }
+
+ bool initialize() {
+ if (!glfwInit()) {
+ std::cerr << "Failed to initialize GLFW" << std::endl;
+ return false;
+ }
+
+ glfwWindowHint(GLFW_CLIENT_API, GLFW_NO_API);
+ glfwWindowHint(GLFW_RESIZABLE, GLFW_TRUE);
+
+ window_ = glfwCreateWindow(WINDOW_WIDTH, WINDOW_HEIGHT,
+ "TimePlot - C++ WebGPU", nullptr, nullptr);
+ if (!window_) {
+ std::cerr << "Failed to create window" << std::endl;
+ return false;
+ }
+
+ glfwSetWindowUserPointer(window_, this);
+ glfwSetKeyCallback(window_, keyCallback);
+ glfwSetFramebufferSizeCallback(window_, resizeCallback);
+
+ if (!initWebGPU()) {
+ return false;
+ }
+
+ renderer_ = std::make_unique<Renderer>(device_, surface_, WINDOW_WIDTH, WINDOW_HEIGHT);
+ if (!renderer_->initialize()) {
+ std::cerr << "Failed to initialize renderer" << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+ void run() {
+ while (!glfwWindowShouldClose(window_)) {
+ glfwPollEvents();
+
+ renderer_->update();
+ renderer_->render();
+ }
+ }
+
+private:
+ bool initWebGPU() {
+ // Create instance
+ instance_ = wgpu::createInstance(wgpu::InstanceDescriptor{});
+ if (!instance_) {
+ std::cerr << "Failed to create WebGPU instance" << std::endl;
+ return false;
+ }
+
+ // Create surface from GLFW window
+ surface_ = glfwCreateWindowWGPUSurface(instance_, window_);
+ if (!surface_) {
+ std::cerr << "Failed to create surface" << std::endl;
+ return false;
+ }
+
+ // Request adapter (synchronous version)
+ wgpu::RequestAdapterOptions adapterOpts{};
+ adapterOpts.compatibleSurface = surface_;
+
+ wgpu::Adapter adapter = instance_.requestAdapter(adapterOpts);
+ if (!adapter) {
+ std::cerr << "Failed to get adapter" << std::endl;
+ return false;
+ }
+
+ // Request device (synchronous version)
+ wgpu::DeviceDescriptor deviceDesc{};
+ device_ = adapter.requestDevice(deviceDesc);
+
+ if (!device_) {
+ std::cerr << "Failed to get device" << std::endl;
+ return false;
+ }
+
+ return true;
+ }
+
+ void cleanup() {
+ renderer_.reset();
+
+ if (window_) {
+ glfwDestroyWindow(window_);
+ window_ = nullptr;
+ }
+ glfwTerminate();
+ }
+
+ static void keyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) {
+ auto* app = static_cast<Application*>(glfwGetWindowUserPointer(window));
+ if (action == GLFW_PRESS) {
+ if (key == GLFW_KEY_ESCAPE) {
+ glfwSetWindowShouldClose(window, GLFW_TRUE);
+ } else if (key == GLFW_KEY_G) {
+ app->renderer_->toggleGrid();
+ }
+ }
+ }
+
+ static void resizeCallback(GLFWwindow* window, int width, int height) {
+ auto* app = static_cast<Application*>(glfwGetWindowUserPointer(window));
+ app->renderer_->resize(width, height);
+ }
+
+ GLFWwindow* window_;
+ wgpu::Instance instance_;
+ wgpu::Device device_;
+ wgpu::Surface surface_;
+ std::unique_ptr<Renderer> renderer_;
+};
+
+int main() {
+ Application app;
+
+ if (!app.initialize()) {
+ return EXIT_FAILURE;
+ }
+
+ app.run();
+ return EXIT_SUCCESS;
+}
diff --git a/cpp-timeplot/src/renderer.cpp b/cpp-timeplot/src/renderer.cpp
new file mode 100644
index 0000000..1c61698
--- /dev/null
+++ b/cpp-timeplot/src/renderer.cpp
@@ -0,0 +1,180 @@
+#include "renderer.h"
+#include "waterfall.h"
+#include <fstream>
+#include <sstream>
+#include <iostream>
+
+Renderer::Renderer(wgpu::Device device, wgpu::Surface surface, int width, int height)
+ : device_(device), surface_(surface), width_(width), height_(height), time_(0.0f),
+ surfaceFormat_(wgpu::TextureFormat::BGRA8Unorm) {}
+
+Renderer::~Renderer() = default;
+
+bool Renderer::initialize() {
+ configureSurface();
+ createPipelines();
+
+ // Create two waterfall views side-by-side
+ waterfalls_.push_back(std::make_unique<Waterfall>(
+ device_, 0.0f, 0.1f, 0.5f, 0.9f, "Frequency vs Time"));
+ waterfalls_.push_back(std::make_unique<Waterfall>(
+ device_, 0.5f, 0.1f, 0.5f, 0.9f, "Position vs Time"));
+
+ for (auto& waterfall : waterfalls_) {
+ if (!waterfall->initialize()) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void Renderer::configureSurface() {
+ wgpu::SurfaceConfiguration config{};
+ config.device = device_;
+ config.format = surfaceFormat_;
+ config.usage = wgpu::TextureUsage::RenderAttachment;
+ config.width = width_;
+ config.height = height_;
+ config.presentMode = wgpu::PresentMode::Fifo;
+ config.alphaMode = wgpu::CompositeAlphaMode::Auto;
+
+ surface_.configure(config);
+}
+
+void Renderer::createPipelines() {
+ // Load shader
+ std::ifstream shaderFile("shaders/waterfall.wgsl");
+ std::stringstream buffer;
+ buffer << shaderFile.rdbuf();
+ std::string shaderCode = buffer.str();
+
+ wgpu::ShaderSourceWGSL wgslSource{};
+ wgslSource.chain.sType = wgpu::SType::ShaderSourceWGSL;
+ wgslSource.code.data = shaderCode.c_str();
+ wgslSource.code.length = shaderCode.length();
+
+ wgpu::ShaderModuleDescriptor shaderDesc{};
+ shaderDesc.nextInChain = &wgslSource.chain;
+ wgpu::ShaderModule shader = device_.createShaderModule(shaderDesc);
+
+ // Vertex buffer layout
+ wgpu::VertexAttribute attributes[2];
+ attributes[0].format = wgpu::VertexFormat::Float32x2;
+ attributes[0].offset = 0;
+ attributes[0].shaderLocation = 0;
+
+ attributes[1].format = wgpu::VertexFormat::Float32x3;
+ attributes[1].offset = 2 * sizeof(float);
+ attributes[1].shaderLocation = 1;
+
+ wgpu::VertexBufferLayout vertexBufferLayout{};
+ vertexBufferLayout.arrayStride = 5 * sizeof(float);
+ vertexBufferLayout.stepMode = wgpu::VertexStepMode::Vertex;
+ vertexBufferLayout.attributeCount = 2;
+ vertexBufferLayout.attributes = attributes;
+
+ // Pipeline layout
+ wgpu::PipelineLayoutDescriptor layoutDesc{};
+ wgpu::PipelineLayout pipelineLayout = device_.createPipelineLayout(layoutDesc);
+
+ // Color target
+ wgpu::BlendState blend{};
+ blend.color.operation = wgpu::BlendOperation::Add;
+ blend.color.srcFactor = wgpu::BlendFactor::One;
+ blend.color.dstFactor = wgpu::BlendFactor::Zero;
+ blend.alpha.operation = wgpu::BlendOperation::Add;
+ blend.alpha.srcFactor = wgpu::BlendFactor::One;
+ blend.alpha.dstFactor = wgpu::BlendFactor::Zero;
+
+ wgpu::ColorTargetState colorTarget{};
+ colorTarget.format = surfaceFormat_;
+ colorTarget.blend = &blend;
+ colorTarget.writeMask = wgpu::ColorWriteMask::All;
+
+ wgpu::FragmentState fragmentState{};
+ fragmentState.module = shader;
+ fragmentState.entryPoint.data = "fs_main";
+ fragmentState.entryPoint.length = 7;
+ fragmentState.targetCount = 1;
+ fragmentState.targets = &colorTarget;
+
+ // Line strip pipeline
+ wgpu::RenderPipelineDescriptor pipelineDesc{};
+ pipelineDesc.layout = pipelineLayout;
+ pipelineDesc.vertex.module = shader;
+ pipelineDesc.vertex.entryPoint.data = "vs_main";
+ pipelineDesc.vertex.entryPoint.length = 7;
+ pipelineDesc.vertex.bufferCount = 1;
+ pipelineDesc.vertex.buffers = &vertexBufferLayout;
+ pipelineDesc.primitive.topology = wgpu::PrimitiveTopology::LineStrip;
+ pipelineDesc.fragment = &fragmentState;
+ pipelineDesc.multisample.count = 1;
+ pipelineDesc.multisample.mask = ~0u;
+
+ linePipeline_ = device_.createRenderPipeline(pipelineDesc);
+
+ // Line list pipeline for grid
+ pipelineDesc.primitive.topology = wgpu::PrimitiveTopology::LineList;
+ lineListPipeline_ = device_.createRenderPipeline(pipelineDesc);
+}
+
+void Renderer::update() {
+ time_ += 0.016f; // ~60fps
+
+ for (auto& waterfall : waterfalls_) {
+ waterfall->update(time_);
+ }
+}
+
+void Renderer::render() {
+ wgpu::SurfaceTexture surfaceTexture;
+ surface_.getCurrentTexture(&surfaceTexture);
+
+ if (surfaceTexture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessOptimal &&
+ surfaceTexture.status != wgpu::SurfaceGetCurrentTextureStatus::SuccessSuboptimal) {
+ std::cerr << "Failed to get surface texture" << std::endl;
+ return;
+ }
+
+ wgpu::Texture texture(surfaceTexture.texture);
+ wgpu::TextureView textureView = texture.createView();
+
+ wgpu::CommandEncoder encoder = device_.createCommandEncoder();
+
+ wgpu::RenderPassColorAttachment colorAttachment{};
+ colorAttachment.view = textureView;
+ colorAttachment.loadOp = wgpu::LoadOp::Clear;
+ colorAttachment.storeOp = wgpu::StoreOp::Store;
+ colorAttachment.clearValue = {0.1, 0.1, 0.15, 1.0};
+
+ wgpu::RenderPassDescriptor renderPassDesc{};
+ renderPassDesc.colorAttachmentCount = 1;
+ renderPassDesc.colorAttachments = &colorAttachment;
+
+ wgpu::RenderPassEncoder pass = encoder.beginRenderPass(renderPassDesc);
+
+ for (auto& waterfall : waterfalls_) {
+ waterfall->render(pass, linePipeline_, lineListPipeline_, width_, height_);
+ }
+
+ pass.end();
+
+ wgpu::CommandBuffer commands = encoder.finish();
+ device_.getQueue().submit(1, &commands);
+ surface_.present();
+}
+
+void Renderer::resize(int width, int height) {
+ if (width > 0 && height > 0) {
+ width_ = width;
+ height_ = height;
+ configureSurface();
+ }
+}
+
+void Renderer::toggleGrid() {
+ for (auto& waterfall : waterfalls_) {
+ waterfall->toggleGrid();
+ }
+}
diff --git a/cpp-timeplot/src/renderer.h b/cpp-timeplot/src/renderer.h
new file mode 100644
index 0000000..3bb2e21
--- /dev/null
+++ b/cpp-timeplot/src/renderer.h
@@ -0,0 +1,35 @@
+#pragma once
+
+#include <webgpu/webgpu.hpp>
+#include <vector>
+#include <memory>
+
+class Waterfall;
+
+class Renderer {
+public:
+ Renderer(wgpu::Device device, wgpu::Surface surface, int width, int height);
+ ~Renderer();
+
+ bool initialize();
+ void update();
+ void render();
+ void resize(int width, int height);
+ void toggleGrid();
+
+private:
+ void configureSurface();
+ void createPipelines();
+
+ wgpu::Device device_;
+ wgpu::Surface surface_;
+ wgpu::TextureFormat surfaceFormat_;
+ wgpu::RenderPipeline linePipeline_;
+ wgpu::RenderPipeline lineListPipeline_;
+
+ int width_;
+ int height_;
+ float time_;
+
+ std::vector<std::unique_ptr<Waterfall>> waterfalls_;
+};
diff --git a/cpp-timeplot/src/waterfall.cpp b/cpp-timeplot/src/waterfall.cpp
new file mode 100644
index 0000000..9baeba6
--- /dev/null
+++ b/cpp-timeplot/src/waterfall.cpp
@@ -0,0 +1,164 @@
+#include "waterfall.h"
+#include <cmath>
+#include <algorithm>
+
+Waterfall::Waterfall(wgpu::Device device, float x, float y, float width, float height, const std::string& title)
+ : device_(device), x_(x), y_(y), width_(width), height_(height), title_(title), showGrid_(true) {}
+
+Waterfall::~Waterfall() = default;
+
+bool Waterfall::initialize() {
+ // Create vertex buffer (large enough for grid, border, and waterfall lines)
+ wgpu::BufferDescriptor bufferDesc{};
+ bufferDesc.size = sizeof(Vertex) * POINTS_PER_LINE * 100;
+ bufferDesc.usage = wgpu::BufferUsage::Vertex | wgpu::BufferUsage::CopyDst;
+ bufferDesc.mappedAtCreation = false;
+
+ vertexBuffer_ = device_.createBuffer(bufferDesc);
+ return vertexBuffer_ != nullptr;
+}
+
+void Waterfall::update(float time) {
+ // Add new line every 10 frames
+ if (static_cast<int>(time * 60.0f) % 10 == 0 && lines_.size() < MAX_LINES) {
+ std::vector<Vertex> line;
+ float phase = time;
+ float freq = 2.0f + std::sin(time * 0.5f) * 1.0f;
+
+ for (int i = 0; i < POINTS_PER_LINE; ++i) {
+ float x = (static_cast<float>(i) / POINTS_PER_LINE) * 2.0f - 1.0f;
+ float y = std::sin(static_cast<float>(i) * 0.1f * freq + phase) * 0.3f;
+
+ float hue = std::fmod(time * 0.1f, 1.0f);
+ Vertex v{
+ {x, y},
+ {
+ std::abs(std::sin(hue * 6.0f)),
+ std::abs(std::sin((hue + 0.33f) * 6.0f)),
+ std::abs(std::sin((hue + 0.66f) * 6.0f))
+ }
+ };
+ line.push_back(v);
+ }
+ lines_.push_back(line);
+ }
+
+ // Scroll lines down
+ for (auto& line : lines_) {
+ for (auto& vertex : line) {
+ vertex.position[1] -= 0.01f;
+ }
+ }
+
+ // Remove lines that scrolled off screen
+ lines_.erase(
+ std::remove_if(lines_.begin(), lines_.end(),
+ [](const std::vector<Vertex>& line) {
+ return !line.empty() && line[0].position[1] < -1.1f;
+ }),
+ lines_.end()
+ );
+}
+
+void Waterfall::render(wgpu::RenderPassEncoder& pass,
+ wgpu::RenderPipeline linePipeline,
+ wgpu::RenderPipeline lineListPipeline,
+ int windowWidth, int windowHeight) {
+ // Set viewport
+ pass.setViewport(
+ x_ * windowWidth,
+ y_ * windowHeight,
+ width_ * windowWidth,
+ height_ * windowHeight,
+ 0.0f, 1.0f
+ );
+
+ // Collect all vertices
+ std::vector<Vertex> allVertices;
+
+ // Border
+ auto borderVertices = generateBorder();
+ size_t borderOffset = allVertices.size();
+ allVertices.insert(allVertices.end(), borderVertices.begin(), borderVertices.end());
+
+ // Grid
+ size_t gridOffset = allVertices.size();
+ size_t gridCount = 0;
+ if (showGrid_) {
+ auto gridVertices = generateGridLines();
+ gridCount = gridVertices.size();
+ allVertices.insert(allVertices.end(), gridVertices.begin(), gridVertices.end());
+ }
+
+ // Waterfall lines
+ size_t linesOffset = allVertices.size();
+ for (const auto& line : lines_) {
+ allVertices.insert(allVertices.end(), line.begin(), line.end());
+ }
+
+ // Upload vertices
+ if (!allVertices.empty()) {
+ device_.getQueue().writeBuffer(vertexBuffer_, 0, allVertices.data(),
+ allVertices.size() * sizeof(Vertex));
+ }
+
+ // Draw border
+ pass.setPipeline(lineListPipeline);
+ pass.setVertexBuffer(0, vertexBuffer_, 0, allVertices.size() * sizeof(Vertex));
+ pass.draw(borderVertices.size(), 1, borderOffset, 0);
+
+ // Draw grid
+ if (showGrid_ && gridCount > 0) {
+ pass.setPipeline(lineListPipeline);
+ pass.draw(gridCount, 1, gridOffset, 0);
+ }
+
+ // Draw waterfall lines
+ if (!lines_.empty()) {
+ pass.setPipeline(linePipeline);
+ for (size_t i = 0; i < lines_.size(); ++i) {
+ uint32_t start = linesOffset + i * POINTS_PER_LINE;
+ pass.draw(POINTS_PER_LINE, 1, start, 0);
+ }
+ }
+}
+
+void Waterfall::toggleGrid() {
+ showGrid_ = !showGrid_;
+}
+
+std::vector<Vertex> Waterfall::generateGridLines() {
+ std::vector<Vertex> vertices;
+ float gridColor[3] = {0.3f, 0.7f, 0.9f};
+
+ // Vertical lines
+ for (int i = 0; i <= 10; ++i) {
+ float x = -1.0f + (static_cast<float>(i) / 10.0f) * 2.0f;
+ vertices.push_back({{x, -1.0f}, {gridColor[0], gridColor[1], gridColor[2]}});
+ vertices.push_back({{x, 1.0f}, {gridColor[0], gridColor[1], gridColor[2]}});
+ }
+
+ // Horizontal lines
+ for (int i = 0; i <= 10; ++i) {
+ float y = -1.0f + (static_cast<float>(i) / 10.0f) * 2.0f;
+ vertices.push_back({{-1.0f, y}, {gridColor[0], gridColor[1], gridColor[2]}});
+ vertices.push_back({{1.0f, y}, {gridColor[0], gridColor[1], gridColor[2]}});
+ }
+
+ return vertices;
+}
+
+std::vector<Vertex> Waterfall::generateBorder() {
+ float borderColor[3] = {0.6f, 0.7f, 0.7f};
+
+ return {
+ {{-1.0f, 1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{1.0f, 1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{1.0f, 1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{1.0f, -1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{1.0f, -1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{-1.0f, -1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{-1.0f, -1.0f}, {borderColor[0], borderColor[1], borderColor[2]}},
+ {{-1.0f, 1.0f}, {borderColor[0], borderColor[1], borderColor[2]}}
+ };
+}
diff --git a/cpp-timeplot/src/waterfall.h b/cpp-timeplot/src/waterfall.h
new file mode 100644
index 0000000..4c5d813
--- /dev/null
+++ b/cpp-timeplot/src/waterfall.h
@@ -0,0 +1,39 @@
+#pragma once
+
+#include <webgpu/webgpu.hpp>
+#include <vector>
+#include <string>
+
+struct Vertex {
+ float position[2];
+ float color[3];
+};
+
+class Waterfall {
+public:
+ Waterfall(wgpu::Device device, float x, float y, float width, float height, const std::string& title);
+ ~Waterfall();
+
+ bool initialize();
+ void update(float time);
+ void render(wgpu::RenderPassEncoder& pass,
+ wgpu::RenderPipeline linePipeline,
+ wgpu::RenderPipeline lineListPipeline,
+ int windowWidth, int windowHeight);
+ void toggleGrid();
+
+private:
+ std::vector<Vertex> generateGridLines();
+ std::vector<Vertex> generateBorder();
+
+ wgpu::Device device_;
+ wgpu::Buffer vertexBuffer_;
+
+ float x_, y_, width_, height_;
+ std::string title_;
+ bool showGrid_;
+
+ std::vector<std::vector<Vertex>> lines_;
+ static constexpr int MAX_LINES = 50;
+ static constexpr int POINTS_PER_LINE = 100;
+};
diff --git a/cpp-timeplot/src/webgpu_impl.cpp b/cpp-timeplot/src/webgpu_impl.cpp
new file mode 100644
index 0000000..e0f099b
--- /dev/null
+++ b/cpp-timeplot/src/webgpu_impl.cpp
@@ -0,0 +1,2 @@
+#define WGPU_CPP_IMPLEMENTATION
+#include <webgpu/webgpu.hpp>