Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions .github/workflows/build-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ jobs:
- name: Install Apt dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake libx11-dev clang-format-14 ccache libvulkan-dev glslang-tools
sudo apt-get install -y build-essential cmake libx11-dev clang-format-14 ccache libvulkan-dev glslang-tools \
libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libxkbcommon-dev \
libwayland-dev wayland-protocols

- name: Install patchelf (Release only)
if: ${{ matrix.build_type == 'Release' }}
Expand Down Expand Up @@ -274,7 +276,9 @@ jobs:
- name: Install Apt dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake libx11-dev clang-format-14 ccache libvulkan-dev glslang-tools
sudo apt-get install -y build-essential cmake libx11-dev clang-format-14 ccache libvulkan-dev glslang-tools \
libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev libxkbcommon-dev \
libwayland-dev wayland-protocols

- name: Install CUDA toolkit
uses: ./.github/actions/setup-cuda
Expand Down
3 changes: 3 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ if(BUILD_EXAMPLES)
add_subdirectory(examples/teleop_ros2)
add_subdirectory(examples/schemaio)
add_subdirectory(examples/native_openxr)
if(BUILD_VIZ)
add_subdirectory(examples/televiz)
endif()
elseif(BUILD_EXAMPLE_TELEOP_ROS2)
add_subdirectory(examples/teleop_ros2)
endif()
Expand Down
26 changes: 26 additions & 0 deletions deps/third_party/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -176,3 +176,29 @@ if(BUILD_VIZ)
FetchContent_MakeAvailable(glm)
message(STATUS "glm 1.0.1 fetched (header-only)")
endif()

# ==============================================================================
# GLFW (window + Vulkan surface for kWindow)
# ==============================================================================
# Owns GLFWwindow + VkSurfaceKHR for VizSession's kWindow display backend.
# Static build to avoid runtime .so dependency.
if(BUILD_VIZ)
message(STATUS "Fetching GLFW from GitHub...")
FetchContent_Declare(
glfw
GIT_REPOSITORY https://github.com/glfw/glfw.git
GIT_TAG 3.4
GIT_SHALLOW TRUE
)
set(GLFW_BUILD_DOCS OFF CACHE BOOL "Skip GLFW docs" FORCE)
set(GLFW_BUILD_TESTS OFF CACHE BOOL "Skip GLFW tests" FORCE)
set(GLFW_BUILD_EXAMPLES OFF CACHE BOOL "Skip GLFW examples" FORCE)
set(GLFW_INSTALL OFF CACHE BOOL "Skip GLFW install target" FORCE)
# Build with both X11 and Wayland (GLFW 3.4 Linux defaults).
# Requires libxrandr-dev / libxinerama-dev / libxcursor-dev /
# libxi-dev / libxext-dev / libxkbcommon-dev plus libwayland-dev /
# wayland-scanner. CI installs them in build-ubuntu.yml. Override
# with -DGLFW_BUILD_WAYLAND=OFF on hosts without Wayland tooling.
FetchContent_MakeAvailable(glfw)
message(STATUS "GLFW 3.4 fetched")
endif()
4 changes: 4 additions & 0 deletions examples/televiz/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

add_subdirectory(window_smoke)
14 changes: 14 additions & 0 deletions examples/televiz/window_smoke/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

cmake_minimum_required(VERSION 3.20)

add_executable(viz_window_smoke main.cpp)

target_link_libraries(viz_window_smoke PRIVATE
viz::session
viz::layers
)

set_target_properties(viz_window_smoke PROPERTIES OUTPUT_NAME "viz_window_smoke")
install(TARGETS viz_window_smoke RUNTIME DESTINATION examples/televiz/window_smoke)
164 changes: 164 additions & 0 deletions examples/televiz/window_smoke/main.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0

// Minimal kWindow demo: opens a 1024x768 GLFW window, fills four
// QuadLayers with solid RGBA patterns tiled 2x2, runs the render
// loop until the window closes.

#include <viz/core/vk_context.hpp>
#include <viz/layers/quad_layer.hpp>
#include <viz/session/viz_session.hpp>

#include <array>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cuda_runtime.h>
#include <memory>
#include <stdexcept>
#include <vector>

namespace
{

struct Rgba
{
uint8_t r, g, b, a;
};

// RAII wrapper around a cudaMalloc'd buffer.
struct CudaDeviceBuffer
{
void* ptr = nullptr;
CudaDeviceBuffer() = default;
explicit CudaDeviceBuffer(size_t bytes)
{
if (cudaMalloc(&ptr, bytes) != cudaSuccess)
{
ptr = nullptr;
throw std::runtime_error("cudaMalloc failed");
}
}
~CudaDeviceBuffer()
{
if (ptr != nullptr)
{
cudaFree(ptr);
}
}
CudaDeviceBuffer(const CudaDeviceBuffer&) = delete;
CudaDeviceBuffer& operator=(const CudaDeviceBuffer&) = delete;
CudaDeviceBuffer(CudaDeviceBuffer&& o) noexcept : ptr(o.ptr)
{
o.ptr = nullptr;
}
CudaDeviceBuffer& operator=(CudaDeviceBuffer&& o) noexcept
{
if (this != &o)
{
if (ptr != nullptr)
{
cudaFree(ptr);
}
ptr = o.ptr;
o.ptr = nullptr;
}
return *this;
}
};

CudaDeviceBuffer make_solid_color_buffer(uint32_t width, uint32_t height, Rgba color)
{
std::vector<Rgba> host(static_cast<size_t>(width) * height, color);
CudaDeviceBuffer buf(host.size() * sizeof(Rgba));
if (cudaMemcpy(buf.ptr, host.data(), host.size() * sizeof(Rgba), cudaMemcpyHostToDevice) != cudaSuccess)
{
throw std::runtime_error("cudaMemcpy failed");
}
return buf;
}

void submit_solid(viz::QuadLayer& layer, void* dev_ptr, uint32_t w, uint32_t h)
{
viz::VizBuffer src{};
src.data = dev_ptr;
src.width = w;
src.height = h;
src.format = viz::PixelFormat::kRGBA8;
src.pitch = static_cast<size_t>(w) * 4;
src.space = viz::MemorySpace::kDevice;
layer.submit(src);
}

} // namespace

int main()
{
constexpr uint32_t kWindowW = 1024;
constexpr uint32_t kWindowH = 768;
constexpr uint32_t kQuadW = 256;
constexpr uint32_t kQuadH = 256;

viz::VizSession::Config cfg{};
cfg.mode = viz::DisplayMode::kWindow;
cfg.window_width = kWindowW;
cfg.window_height = kWindowH;
cfg.app_name = "viz_window_smoke";
// Dark grey clear so letterbox margins are visible against the quads.
cfg.clear_color[0] = 0.1f;
cfg.clear_color[1] = 0.1f;
cfg.clear_color[2] = 0.1f;
cfg.clear_color[3] = 1.0f;

try
{
auto session = viz::VizSession::create(cfg);
const viz::VkContext* ctx = session->get_vk_context();
const VkRenderPass render_pass = session->get_render_pass();

const std::array<Rgba, 4> palette = { {
{ 220, 60, 60, 255 }, // red
{ 60, 220, 60, 255 }, // green
{ 60, 100, 220, 255 }, // blue
{ 220, 220, 220, 255 }, // white
} };

// RAII: buffers freed on scope exit (normal or exception).
// Outlive the session — submit() copies into the mailbox, so
// the device pointers can be freed any time after.
std::vector<CudaDeviceBuffer> device_buffers;
device_buffers.reserve(palette.size());
for (size_t i = 0; i < palette.size(); ++i)
{
viz::QuadLayer::Config layer_cfg;
layer_cfg.name = "smoke_quad_" + std::to_string(i);
layer_cfg.resolution = { kQuadW, kQuadH };
auto* layer = session->add_layer<viz::QuadLayer>(*ctx, render_pass, layer_cfg);

device_buffers.push_back(make_solid_color_buffer(kQuadW, kQuadH, palette[i]));
submit_solid(*layer, device_buffers.back().ptr, kQuadW, kQuadH);
}

// Print fps once per second (60 frames at 60Hz) so resize /
// move stalls show up as drops in the terminal output.
while (!session->should_close())
{
const auto info = session->render();
if (info.frame_index > 0 && info.frame_index % 60 == 0)
{
const auto stats = session->get_frame_timing_stats();
std::printf("frame %llu: %.1f fps (%.2f ms/frame)\n", static_cast<unsigned long long>(info.frame_index),
stats.render_fps, stats.avg_frame_time_ms);
std::fflush(stdout);
}
}

session.reset(); // tear down before buffers go out of scope
}
catch (const std::exception& e)
{
std::fprintf(stderr, "viz_window_smoke: %s\n", e.what());
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}
6 changes: 6 additions & 0 deletions src/viz/core/cpp/inc/viz/core/render_target.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,11 @@ class RenderTarget
return resolution_;
}

// Recreate color/depth/framebuffer at new_size. Keeps the render
// pass alive; pipelines built against it stay valid. Caller must
// ensure GPU work is retired (vkDeviceWaitIdle / fence wait).
void resize(Resolution new_size);

private:
explicit RenderTarget(const VkContext& ctx);

Expand All @@ -104,6 +109,7 @@ class RenderTarget
void create_depth_image(const Config& config);
void create_render_pass();
void create_framebuffer();
void destroy_attachments(); // images + views + memory + framebuffer

const VkContext* ctx_ = nullptr;

Expand Down
16 changes: 16 additions & 0 deletions src/viz/core/cpp/inc/viz/core/viz_types.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,16 @@ struct Resolution
uint32_t height = 0;
};

// 2D pixel-coordinate rectangle. Mirrors VkRect2D (offset + extent) but
// stays Vulkan-free so viz_types.hpp doesn't pull in vulkan.h.
struct Rect2D
{
int32_t x = 0;
int32_t y = 0;
uint32_t width = 0;
uint32_t height = 0;
};

// 3D pose in OpenXR stage space: right-handed, Y-up, meters for distance,
// orientation as a unit quaternion. Default-constructed is identity.
//
Expand Down Expand Up @@ -55,6 +65,12 @@ struct ViewInfo
glm::mat4 projection_matrix{ 1.0f }; // identity
Fov fov{};
Pose3D pose{};
// Pixel rect in the framebuffer the layer should draw into for
// this view. Filled by the compositor before record(). In window
// mode it's the layer's aspect-fit content rect inside its tile;
// in XR stereo it's the eye's subImage.imageRect; in offscreen
// it's the full target.
Rect2D viewport{};
};

} // namespace viz
61 changes: 56 additions & 5 deletions src/viz/core/cpp/render_target.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,16 +96,22 @@ void RenderTarget::destroy()
{
return;
}
if (framebuffer_ != VK_NULL_HANDLE)
{
vkDestroyFramebuffer(device, framebuffer_, nullptr);
framebuffer_ = VK_NULL_HANDLE;
}
destroy_attachments();
if (render_pass_ != VK_NULL_HANDLE)
{
vkDestroyRenderPass(device, render_pass_, nullptr);
render_pass_ = VK_NULL_HANDLE;
}
}

void RenderTarget::destroy_attachments()
{
const VkDevice device = ctx_->device();
if (framebuffer_ != VK_NULL_HANDLE)
{
vkDestroyFramebuffer(device, framebuffer_, nullptr);
framebuffer_ = VK_NULL_HANDLE;
}
if (depth_view_ != VK_NULL_HANDLE)
{
vkDestroyImageView(device, depth_view_, nullptr);
Expand Down Expand Up @@ -138,6 +144,51 @@ void RenderTarget::destroy()
}
}

void RenderTarget::resize(Resolution new_size)
{
if (new_size.width == 0 || new_size.height == 0)
{
return;
}
if (new_size.width == resolution_.width && new_size.height == resolution_.height)
{
return;
}
const Resolution old_size = resolution_;
destroy_attachments();
resolution_ = new_size;
Config c{};
c.resolution = new_size;
try
{
create_color_image(c);
create_depth_image(c);
create_framebuffer();
}
catch (...)
{
// Restore the old attachments so the object stays usable.
// If the restore itself fails, drop everything — caller has
// to recreate the render target.
destroy_attachments();
resolution_ = old_size;
try
{
Config old_c{};
old_c.resolution = old_size;
create_color_image(old_c);
create_depth_image(old_c);
create_framebuffer();
}
catch (...)
{
destroy_attachments();
resolution_ = Resolution{};
}
throw;
}
}
Comment thread
farbod-nv marked this conversation as resolved.

void RenderTarget::create_color_image(const Config& config)
{
const VkDevice device = ctx_->device();
Expand Down
Loading
Loading