//===----------------------------------------------------------------------===// // // Part of libcu++, the C++ Standard Library for your entire system, // under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. // //===----------------------------------------------------------------------===// /** * Image processing pipeline — all runtime API usage in one file. * * This file contains the complete pipeline: * - Device selection and tile sizing * - Memory pool creation and buffer allocation * - Tile upload/download with copy_bytes and fill_bytes * - CUB-based processing (histogram, equalization, thresholding, reduction) * - GPU downscale with CUB BlockReduce * - Double-buffered two-stream orchestration * * Supporting details (image generation, printing) are in detail.cu. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include // ═════════════════════════════════════════════════════════════════════ // Device selection // ═════════════════════════════════════════════════════════════════════ static device_plan select_device_and_plan() { std::cout << "=== Device selection ===\n"; cuda::device_ref best = cuda::devices[0]; size_t best_mem = 0; for (auto dev : cuda::devices) { const size_t total_bytes = dev.attribute(cuda::device_attributes::total_global_memory); const int sms = dev.attribute(cuda::device_attributes::multiprocessor_count); const auto name = dev.name(); std::cout << " [" << dev.get() << "] "; std::cout.write(name.data(), static_cast(name.size())); std::cout << " " << std::setw(3) << sms << " SMs " << std::fixed << std::setprecision(0) << total_bytes / (1024.0 * 1024.0) << " MB\n" << std::defaultfloat << std::setprecision(6); if (total_bytes > best_mem) { best = dev; best_mem = total_bytes; } } const auto cc = best.attribute(cuda::device_attributes::compute_capability); const auto traits = cuda::arch_traits_for(cc); print_device_info(best, traits, best_mem); // Budget 60% of total GPU memory for the per-tile working set. const size_t budget = static_cast(best_mem * 0.60); const size_t overhead = 128 * 1024 * 1024; const size_t bytes_per_pixel = 4 * sizeof(pixel_t) + 2 * sizeof(float); const size_t usable_budget = (budget > overhead) ? (budget - overhead) : 0; const size_t budget_rows = usable_budget / bytes_per_pixel / image_width; constexpr int tile_alignment = preview_scale; const size_t max_launch_rows = (static_cast(cuda::std::numeric_limits::max()) / image_width / tile_alignment) * tile_alignment; const size_t max_tile_rows = cuda::std::min(static_cast(image_height), max_launch_rows); const size_t aligned_tile_rows = (budget_rows / tile_alignment) * tile_alignment; const auto clamped_tile_rows = cuda::std::clamp(aligned_tile_rows, static_cast(tile_alignment), max_tile_rows); const int tile_rows = static_cast(clamped_tile_rows); const int num_tiles = cuda::ceil_div(image_height, tile_rows); print_tile_plan(tile_rows, tile_alignment, num_tiles, budget, best_mem); return {best, tile_rows, num_tiles, budget}; } // ═════════════════════════════════════════════════════════════════════ // Buffer allocation // ═════════════════════════════════════════════════════════════════════ static tile_buffers allocate_tile_buffers(cuda::stream_ref stream, cuda::device_ref device, int tile_rows, size_t gpu_budget, int num_tiles) { std::cout << "=== Tile buffer allocation ===\n"; const size_t tile_pixels = static_cast(tile_rows) * image_width; const size_t preview_px = (tile_rows / preview_scale) * (image_width / preview_scale); // Single pool for our buffers and CUB temporaries. const size_t device_total = 2 * tile_pixels * sizeof(pixel_t) // double-buffered pixel tiles + 2 * tile_pixels * sizeof(pixel_t) // double-buffered equalized tiles + 2 * tile_pixels * sizeof(float) // double-buffered normalized float tiles + sizeof(float4) // reduction output + num_bins * sizeof(pixel_t) // equalization LUT + 2 * num_bins * sizeof(int) // double-buffered histograms + 2 * preview_px * sizeof(pixel_t); // double-buffered preview tiles cuda::memory_pool_properties props{}; props.initial_pool_size = device_total; props.max_pool_size = gpu_budget; auto device_pool = cuda::mr::shared_resource( cuda::std::in_place_type, device, props); // Device buffers — no_init since kernels/copies write before reading. auto dev_tile_0 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_tile_1 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_float_0 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_float_1 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_hist_0 = cuda::make_buffer(stream, device_pool, num_bins, cuda::no_init); auto dev_hist_1 = cuda::make_buffer(stream, device_pool, num_bins, cuda::no_init); auto dev_tile_stats = cuda::make_buffer(stream, device_pool, num_tiles, cuda::no_init); auto dev_lut = cuda::make_buffer(stream, device_pool, num_bins, cuda::no_init); auto dev_equalized_0 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_equalized_1 = cuda::make_buffer(stream, device_pool, tile_pixels, cuda::no_init); auto dev_preview_0 = cuda::make_buffer(stream, device_pool, preview_px, cuda::no_init); auto dev_preview_1 = cuda::make_buffer(stream, device_pool, preview_px, cuda::no_init); // Pinned host buffers — make_pinned_buffer uses the default pinned pool. auto host_image = cuda::make_pinned_buffer(stream, image_pixels, pixel_t{0}); auto host_tile_hists = cuda::make_pinned_buffer(stream, static_cast(num_tiles) * num_bins, int{0}); auto host_tile_stats = cuda::make_pinned_buffer(stream, num_tiles, float4{0, 0, 0, 0}); print_allocation_info(device_total, gpu_budget, tile_pixels, tile_rows); return { {cuda::std::move(dev_tile_0), cuda::std::move(dev_tile_1)}, {cuda::std::move(dev_float_0), cuda::std::move(dev_float_1)}, {cuda::std::move(dev_hist_0), cuda::std::move(dev_hist_1)}, cuda::std::move(dev_tile_stats), cuda::std::move(dev_lut), {cuda::std::move(dev_equalized_0), cuda::std::move(dev_equalized_1)}, cuda::std::move(host_image), cuda::std::move(host_tile_hists), cuda::std::move(host_tile_stats), {cuda::std::move(dev_preview_0), cuda::std::move(dev_preview_1)}, device_pool, tile_pixels, }; } // ═════════════════════════════════════════════════════════════════════ // Tile transfer helpers // ═════════════════════════════════════════════════════════════════════ static size_t upload_tile(cuda::stream_ref stream, tile_buffers& bufs, int slot, int tile_idx, int tile_rows) { const size_t offset = static_cast(tile_idx) * bufs.tile_pixels; const size_t count = cuda::std::min(bufs.tile_pixels, image_pixels - offset); cuda::copy_configuration config{}; config.src_access_order = cuda::source_access_order::stream; cuda::copy_bytes(stream, bufs.host_image.subspan(offset, count), bufs.dev_tile[slot].first(count), config); cuda::fill_bytes(stream, bufs.dev_histogram[slot], uint8_t{0}); return count; } static void download_tile_histogram(cuda::stream_ref stream, tile_buffers& bufs, int slot, int tile_idx) { const size_t offset = static_cast(tile_idx) * num_bins; cuda::copy_bytes(stream, bufs.dev_histogram[slot], bufs.host_tile_histograms.subspan(offset, num_bins)); } static void accumulate_histograms(tile_buffers& bufs, int num_tiles, cuda::std::span result) { for (int i = 0; i < num_bins; ++i) { result[i] = 0; } for (int t = 0; t < num_tiles; ++t) { const size_t offset = static_cast(t) * num_bins; for (int i = 0; i < num_bins; ++i) { result[i] += bufs.host_tile_histograms.get_unsynchronized(offset + i); } } } static void upload_lut(cuda::stream_ref stream, tile_buffers& bufs, cuda::std::span host_lut) { cuda::copy_bytes(stream, host_lut, bufs.dev_lut); } // ═════════════════════════════════════════════════════════════════════ // CUB-based processing // ═════════════════════════════════════════════════════════════════════ static auto make_cub_env(cuda::stream_ref stream, cuda::mr::shared_resource& pool) { const auto mr_prop = cuda::std::execution::prop{cuda::mr::get_memory_resource_t{}, pool}; return cuda::std::execution::env{stream, mr_prop}; } static void check_cub(cudaError_t err, const char* msg) { if (err != cudaSuccess) { throw cuda::cuda_error(err, msg, "CUB"); } } // ── Processing functions ───────────────────────────────────────────── static void compute_histogram(cuda::stream_ref stream, tile_buffers& bufs, int slot, size_t tile_pixel_count) { auto env = make_cub_env(stream, bufs.device_pool); check_cub( cub::DeviceHistogram::HistogramEven( bufs.dev_tile[slot].first(tile_pixel_count).data(), bufs.dev_histogram[slot].data(), num_levels, 0, num_bins, static_cast(tile_pixel_count), env), "HistogramEven (pass 1)"); } static void process_tile( cuda::stream_ref stream, tile_buffers& bufs, int slot, int tile_idx, size_t tile_pixel_count, float threshold) { const int n = static_cast(tile_pixel_count); auto pixel_data = bufs.dev_tile[slot].first(tile_pixel_count).data(); auto eq_data = bufs.dev_equalized[slot].data(); auto float_data = bufs.dev_float_tile[slot].data(); auto env = make_cub_env(stream, bufs.device_pool); // Equalize: apply the LUT to remap pixel intensities. auto lut_span = bufs.dev_lut.first(num_bins); auto equalize = [lut_span] __device__(pixel_t p) -> pixel_t { return lut_span[p]; }; check_cub(cub::DeviceTransform::Transform(pixel_data, eq_data, n, equalize, env), "Transform (equalize)"); // Normalize: convert uint8 pixels to [0, 1] floats. auto normalize = [] __device__(pixel_t p) -> float { constexpr float pixel_max = cuda::std::numeric_limits::max(); return static_cast(p) / pixel_max; }; check_cub(cub::DeviceTransform::Transform(eq_data, float_data, n, normalize, env), "Transform (normalize)"); check_cub( cub::DeviceHistogram::HistogramEven(eq_data, bufs.dev_histogram[slot].data(), num_levels, 0, num_bins, n, env), "HistogramEven (pass 2)"); // Combined threshold + count/min/max/sum in a single pass via float4: x=count, y=min, z=max, w=sum. // Each tile writes to its own output index — no sync needed between tiles. constexpr float flt_max = cuda::std::numeric_limits::max(); constexpr float flt_low = cuda::std::numeric_limits::lowest(); const float4 identity{0.0f, flt_max, flt_low, 0.0f}; auto threshold_stats = [threshold] __device__(float v) -> float4 { if (v > threshold) { return {1.0f, v, v, v}; // count=1, min=v, max=v, sum=v } return {0.0f, flt_max, flt_low, 0.0f}; }; auto stats_reduce = [] __device__(float4 a, float4 b) -> float4 { return {a.x + b.x, cuda::std::min(a.y, b.y), cuda::std::max(a.z, b.z), a.w + b.w}; }; check_cub(cub::DeviceReduce::TransformReduce( float_data, bufs.dev_tile_stats.data() + tile_idx, n, stats_reduce, threshold_stats, identity, env), "TransformReduce"); // D2H copy into this tile's slot — no sync, read after all tiles finish. cuda::copy_bytes(stream, bufs.dev_tile_stats.subspan(tile_idx, 1), bufs.host_tile_stats.subspan(tile_idx, 1)); } /// Accumulate per-tile stats into a single result after all tiles finish. static tile_stats accumulate_tile_stats(tile_buffers& bufs, int num_tiles) { tile_stats result{}; result.min_val = cuda::std::numeric_limits::max(); result.max_val = cuda::std::numeric_limits::lowest(); for (int t = 0; t < num_tiles; ++t) { const auto s = bufs.host_tile_stats.get_unsynchronized(t); result.num_selected += static_cast(s.x); result.min_val = cuda::std::min(result.min_val, s.y); result.max_val = cuda::std::max(result.max_val, s.z); result.sum += s.w; } return result; } // ── Host-side algorithms ───────────────────────────────────────────── static float compute_otsu_threshold(cuda::std::span histogram, size_t total_pixels) { double total_sum = 0; for (int i = 0; i < num_bins; ++i) { total_sum += static_cast(i) * histogram[i]; } double sum_bg = 0, weight_bg = 0, max_var = 0; int best_t = 0; for (int t = 0; t < num_bins; ++t) { weight_bg += histogram[t]; if (weight_bg == 0) { continue; } const double weight_fg = static_cast(total_pixels) - weight_bg; if (weight_fg == 0) { break; } sum_bg += static_cast(t) * histogram[t]; const double mean_bg = sum_bg / weight_bg; const double mean_fg = (total_sum - sum_bg) / weight_fg; const double var = weight_bg * weight_fg * (mean_bg - mean_fg) * (mean_bg - mean_fg); if (var > max_var) { max_var = var; best_t = t; } } return static_cast(best_t) / cuda::std::numeric_limits::max(); } static void build_equalization_lut( cuda::std::span histogram, size_t total_pixels, cuda::std::span lut_out) { constexpr double max_val = cuda::std::numeric_limits::max(); const double scale = max_val / static_cast(total_pixels); double cdf = 0; for (int i = 0; i < num_bins; ++i) { cdf += histogram[i]; lut_out[i] = static_cast(cuda::std::min(cdf * scale, max_val)); } } // ═════════════════════════════════════════════════════════════════════ // Downscale kernel // ═════════════════════════════════════════════════════════════════════ // Each block produces one output pixel by box-averaging a scale×scale // source block. Threads cooperatively load source pixels and sum // them locally, then cub::BlockReduce merges the partial sums. // // The block size is extracted from the launch config at compile time // via cuda::gpu_thread.count(cuda::block, config), which is then used // as the template parameter for cub::BlockReduce. struct downscale_kernel { template __device__ void operator()(Config config, cuda::std::span src, cuda::std::span dst, int src_width, int scale) { constexpr int block_size = cuda::gpu_thread.count(cuda::block, config); const int out_idx = blockIdx.x; if (out_idx >= static_cast(dst.size())) { return; } const int dst_width = src_width / scale; const int px = out_idx % dst_width; const int py = out_idx / dst_width; const int total_elems = scale * scale; // Each thread sums its share of the scale×scale source block. int local_sum = 0; const int tid = threadIdx.x; for (int i = tid; i < total_elems; i += block_size) { const int dy = i / scale; const int dx = i % scale; local_sum += src[static_cast(py * scale + dy) * src_width + (px * scale + dx)]; } // cub::BlockReduce sums the per-thread partial sums into a single // block-wide total. Without CUB, this would be a manual shared- // memory tree reduction: // // __shared__ int smem[block_size]; // smem[tid] = local_sum; // __syncthreads(); // for (int s = block_size / 2; s > 0; s >>= 1) // { // if (tid < s) smem[tid] += smem[tid + s]; // __syncthreads(); // } // int block_sum = smem[0]; using BlockReduceT = cub::BlockReduce; __shared__ typename BlockReduceT::TempStorage temp_storage; const int block_sum = BlockReduceT(temp_storage).Sum(local_sum); if (tid == 0) { dst[out_idx] = static_cast(block_sum / total_elems); } } }; void downscale_tile( cuda::stream_ref stream, tile_buffers& bufs, int slot, cuda::std::span dev_src, int row_offset, int tile_rows, cuda::std::span host_preview) { const int dst_rows = tile_rows / preview_scale; const int dst_cols = image_width / preview_scale; const int dst_pixels = dst_rows * dst_cols; if (dst_pixels == 0) { return; } constexpr int block_size = 256; const auto config = cuda::make_config(cuda::block_dims(), cuda::grid_dims(dst_pixels)); cuda::launch( stream, config, downscale_kernel{}, dev_src, bufs.dev_preview[slot].first(static_cast(dst_pixels)), image_width, preview_scale); const int preview_row_offset = row_offset / preview_scale; cuda::copy_bytes( stream, bufs.dev_preview[slot].first(static_cast(dst_pixels)), host_preview.subspan(static_cast(preview_row_offset) * dst_cols, static_cast(dst_pixels))); } // ═════════════════════════════════════════════════════════════════════ // Main // ═════════════════════════════════════════════════════════════════════ int main() try { // ── 1. Device selection and tile sizing ──────────────────────────── const auto plan = select_device_and_plan(); // ── 2. Allocate all buffers ──────────────────────────────────────── // Two streams for double-buffered tile processing. stream_a also // handles setup work (allocation, LUT upload, etc.) between passes. cuda::stream stream_a{plan.device}; cuda::stream stream_b{plan.device}; cuda::stream_ref streams[2] = {stream_a, stream_b}; auto bufs = allocate_tile_buffers(stream_a, plan.device, plan.tile_rows, plan.gpu_budget, plan.num_tiles); // ── 3. Generate image and downscale input preview ────────────────── const int pw = image_width / preview_scale; const int ph = image_height / preview_scale; auto host_input_preview = cuda::make_pinned_buffer(stream_a, static_cast(pw) * ph, pixel_t{0}); generate_image(stream_a, bufs, plan.num_tiles, host_input_preview.subspan(0)); bool outputs_ok = write_bmp("input_preview.bmp", host_input_preview.subspan(0), pw, ph); // ── 4. Pass 1: histogram (double-buffered) ───────────────────────── // stream_a: [upload tile 0] [histogram 0] [download 0] [tile 2] ... // stream_b: [upload tile 1] [histogram 1] [download 1] ... std::cout << "=== Pass 1: histogram ===\n"; cuda::timed_event pass1_start{stream_a}; for (int t = 0; t < plan.num_tiles; ++t) { const int slot = t % 2; const size_t count = upload_tile(streams[slot], bufs, slot, t, plan.tile_rows); compute_histogram(streams[slot], bufs, slot, count); download_tile_histogram(streams[slot], bufs, slot, t); } stream_a.wait(stream_b); cuda::timed_event pass1_end{stream_a}; stream_a.sync(); const double pass1_ms = (pass1_end - pass1_start).count() / 1e6; std::cout << std::fixed << std::setprecision(1) << " Histogram pass: " << pass1_ms << " ms\n" << std::defaultfloat << std::setprecision(6); // ── 5. Otsu threshold + equalization LUT ─────────────────────────── cuda::std::array original_hist{}; cuda::std::span global_hist_span{original_hist}; accumulate_histograms(bufs, plan.num_tiles, global_hist_span); const float otsu = compute_otsu_threshold(global_hist_span, image_pixels); std::cout << std::fixed << std::setprecision(4) << " Otsu threshold: " << otsu << " (" << static_cast(otsu * 255) << " / 255)\n" << std::defaultfloat << std::setprecision(6); pixel_t host_lut[num_bins]; build_equalization_lut(global_hist_span, image_pixels, cuda::std::span(host_lut, num_bins)); // Construct a pinned buffer from the host LUT array — the buffer // copies the data in stream order, no manual sync needed. auto pinned_lut = cuda::make_pinned_buffer(stream_a, host_lut, host_lut + num_bins); upload_lut(stream_a, bufs, pinned_lut.subspan(0)); std::cout << " Equalization LUT uploaded\n\n"; cuda::fill_bytes(stream_a, bufs.host_tile_histograms, uint8_t{0}); auto host_eq_preview = cuda::make_pinned_buffer(stream_a, static_cast(pw) * ph, pixel_t{0}); // stream_b must wait for the setup work on stream_a before starting pass 2. stream_b.wait(stream_a); // ── 6. Pass 2: equalize + threshold + stats + preview ────────────── std::cout << "=== Pass 2: equalize + threshold + statistics ===\n"; cuda::timed_event pass2_start{stream_a}; for (int t = 0; t < plan.num_tiles; ++t) { const int slot = t % 2; const size_t count = upload_tile(streams[slot], bufs, slot, t, plan.tile_rows); process_tile(streams[slot], bufs, slot, t, count, otsu); const int tile_rows = static_cast(count / image_width); const int row_offset = t * static_cast(bufs.tile_pixels / image_width); downscale_tile( streams[slot], bufs, slot, bufs.dev_equalized[slot].first(count), row_offset, tile_rows, host_eq_preview.subspan(0)); download_tile_histogram(streams[slot], bufs, slot, t); } // Single sync after all tiles — stats, histograms, and preview are on host. stream_a.wait(stream_b); cuda::timed_event pass2_end{stream_a}; stream_a.sync(); const double pass2_ms = (pass2_end - pass2_start).count() / 1e6; const auto stats = accumulate_tile_stats(bufs, plan.num_tiles); const double mean_selected = (stats.num_selected > 0) ? static_cast(stats.sum) / stats.num_selected : 0.0; print_pass_stats(pass2_ms, stats.num_selected, mean_selected, stats.min_val, stats.max_val); print_pool_stats(bufs); // ── 7. Write equalized preview ───────────────────────────────────── outputs_ok = write_bmp("equalized_preview.bmp", host_eq_preview.subspan(0), pw, ph) && outputs_ok; if (!outputs_ok) { std::cerr << "One or more preview BMP files were not written.\n"; } std::cout << '\n'; // ── 8. Sanity check ──────────────────────────────────────────────── const auto orig_iqr = compute_iqr(cuda::std::span{original_hist}, image_pixels); cuda::std::array equalized_hist{}; accumulate_histograms(bufs, plan.num_tiles, cuda::std::span{equalized_hist}); const auto eq_iqr = compute_iqr(cuda::std::span{equalized_hist}, image_pixels); print_sanity_check(orig_iqr, eq_iqr); const bool ok = outputs_ok && eq_iqr.width() > orig_iqr.width(); print_summary(plan.num_tiles, plan.tile_rows, pass1_ms, pass2_ms, ok); return ok ? 0 : 1; } catch (const cuda::cuda_error& e) { std::cerr << "CUDA error: " << e.what() << '\n'; return 1; } catch (const std::exception& e) { std::cerr << "Error: " << e.what() << '\n'; return 1; } catch (...) { std::cerr << "An unknown error was encountered\n"; return 1; }