CCCL (CUDA C++ Core Libraries) provides: - CUB: device/block/warp-level GPU primitives (reduce, scan, sort, topk) - Thrust: high-level parallel algorithms (transform_reduce, sort, scan) - libcudacxx: CUDA C++ standard library (atomics, barriers, memory) - cudax: experimental features (memory resources, allocators) - Tuning policies: per-SM hardware-specific algorithm parameters Competition optimization vectors mapped to CCCL: - Output TPS (83% weight): warp_reduce, block_reduce, device_topk - Input TPS (14% weight): device_scan, block_load, prefetch - Cache TPS (3% weight): prefix caching strategy patterns - Memory (0.9 util): pooled/cached/buddy allocators Source: https://github.com/NVIDIA/cccl (shallow clone, HEAD only) License: Apache-2.0
617 lines
26 KiB
Plaintext
617 lines
26 KiB
Plaintext
//===----------------------------------------------------------------------===//
|
||
//
|
||
// 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 <cub/block/block_reduce.cuh>
|
||
#include <cub/device/device_histogram.cuh>
|
||
#include <cub/device/device_reduce.cuh>
|
||
#include <cub/device/device_transform.cuh>
|
||
|
||
#include <cuda/algorithm>
|
||
#include <cuda/buffer>
|
||
#include <cuda/cmath>
|
||
#include <cuda/devices>
|
||
#include <cuda/launch>
|
||
#include <cuda/memory_pool>
|
||
#include <cuda/memory_resource>
|
||
#include <cuda/std/__exception/cuda_error.h>
|
||
#include <cuda/std/algorithm>
|
||
#include <cuda/std/array>
|
||
#include <cuda/std/execution>
|
||
#include <cuda/std/functional>
|
||
#include <cuda/std/limits>
|
||
#include <cuda/std/span>
|
||
#include <cuda/stream>
|
||
|
||
#include <exception>
|
||
#include <iomanip>
|
||
#include <iostream>
|
||
|
||
#include <cuda_runtime_api.h>
|
||
#include <detail.h>
|
||
#include <image_pipeline.h>
|
||
|
||
// ═════════════════════════════════════════════════════════════════════
|
||
// 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<std::streamsize>(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<size_t>(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<size_t>(cuda::std::numeric_limits<int>::max()) / image_width / tile_alignment) * tile_alignment;
|
||
const size_t max_tile_rows = cuda::std::min(static_cast<size_t>(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<size_t>(tile_alignment), max_tile_rows);
|
||
const int tile_rows = static_cast<int>(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<size_t>(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::device_memory_pool>(
|
||
cuda::std::in_place_type<cuda::device_memory_pool>, device, props);
|
||
|
||
// Device buffers — no_init since kernels/copies write before reading.
|
||
auto dev_tile_0 = cuda::make_buffer<pixel_t>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_tile_1 = cuda::make_buffer<pixel_t>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_float_0 = cuda::make_buffer<float>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_float_1 = cuda::make_buffer<float>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_hist_0 = cuda::make_buffer<int>(stream, device_pool, num_bins, cuda::no_init);
|
||
auto dev_hist_1 = cuda::make_buffer<int>(stream, device_pool, num_bins, cuda::no_init);
|
||
auto dev_tile_stats = cuda::make_buffer<float4>(stream, device_pool, num_tiles, cuda::no_init);
|
||
auto dev_lut = cuda::make_buffer<pixel_t>(stream, device_pool, num_bins, cuda::no_init);
|
||
auto dev_equalized_0 = cuda::make_buffer<pixel_t>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_equalized_1 = cuda::make_buffer<pixel_t>(stream, device_pool, tile_pixels, cuda::no_init);
|
||
auto dev_preview_0 = cuda::make_buffer<pixel_t>(stream, device_pool, preview_px, cuda::no_init);
|
||
auto dev_preview_1 = cuda::make_buffer<pixel_t>(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<pixel_t>(stream, image_pixels, pixel_t{0});
|
||
auto host_tile_hists = cuda::make_pinned_buffer<int>(stream, static_cast<size_t>(num_tiles) * num_bins, int{0});
|
||
auto host_tile_stats = cuda::make_pinned_buffer<float4>(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<size_t>(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<size_t>(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<histogram_count_t> 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<size_t>(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<const pixel_t> 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<cuda::device_memory_pool>& 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<int>(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<int>(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<pixel_t>::max();
|
||
return static_cast<float>(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<float>::max();
|
||
constexpr float flt_low = cuda::std::numeric_limits<float>::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<float>::max();
|
||
result.max_val = cuda::std::numeric_limits<float>::lowest();
|
||
|
||
for (int t = 0; t < num_tiles; ++t)
|
||
{
|
||
const auto s = bufs.host_tile_stats.get_unsynchronized(t);
|
||
result.num_selected += static_cast<long long>(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<const histogram_count_t> histogram, size_t total_pixels)
|
||
{
|
||
double total_sum = 0;
|
||
for (int i = 0; i < num_bins; ++i)
|
||
{
|
||
total_sum += static_cast<double>(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<double>(total_pixels) - weight_bg;
|
||
if (weight_fg == 0)
|
||
{
|
||
break;
|
||
}
|
||
sum_bg += static_cast<double>(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<float>(best_t) / cuda::std::numeric_limits<pixel_t>::max();
|
||
}
|
||
|
||
static void build_equalization_lut(
|
||
cuda::std::span<const histogram_count_t> histogram, size_t total_pixels, cuda::std::span<pixel_t> lut_out)
|
||
{
|
||
constexpr double max_val = cuda::std::numeric_limits<pixel_t>::max();
|
||
const double scale = max_val / static_cast<double>(total_pixels);
|
||
double cdf = 0;
|
||
for (int i = 0; i < num_bins; ++i)
|
||
{
|
||
cdf += histogram[i];
|
||
lut_out[i] = static_cast<pixel_t>(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 <typename Config>
|
||
__device__ void
|
||
operator()(Config config, cuda::std::span<const pixel_t> src, cuda::std::span<pixel_t> 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<int>(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<size_t>(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<int, block_size>;
|
||
__shared__ typename BlockReduceT::TempStorage temp_storage;
|
||
const int block_sum = BlockReduceT(temp_storage).Sum(local_sum);
|
||
|
||
if (tid == 0)
|
||
{
|
||
dst[out_idx] = static_cast<pixel_t>(block_sum / total_elems);
|
||
}
|
||
}
|
||
};
|
||
|
||
void downscale_tile(
|
||
cuda::stream_ref stream,
|
||
tile_buffers& bufs,
|
||
int slot,
|
||
cuda::std::span<const pixel_t> dev_src,
|
||
int row_offset,
|
||
int tile_rows,
|
||
cuda::std::span<pixel_t> 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<block_size>(), cuda::grid_dims(dst_pixels));
|
||
|
||
cuda::launch(
|
||
stream,
|
||
config,
|
||
downscale_kernel{},
|
||
dev_src,
|
||
bufs.dev_preview[slot].first(static_cast<size_t>(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<size_t>(dst_pixels)),
|
||
host_preview.subspan(static_cast<size_t>(preview_row_offset) * dst_cols, static_cast<size_t>(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<pixel_t>(stream_a, static_cast<size_t>(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<histogram_count_t, num_bins> 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<int>(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<pixel_t>(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<pixel_t>(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<pixel_t>(stream_a, static_cast<size_t>(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<int>(count / image_width);
|
||
const int row_offset = t * static_cast<int>(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<double>(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<histogram_count_t, num_bins> 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;
|
||
}
|