From 0bf6519f24e19a2d6f24d3c41faa3f38c865137f Mon Sep 17 00:00:00 2001 From: evanokeeffe Date: Tue, 8 Sep 2026 14:51:00 +0100 Subject: [PATCH 1/3] This helped with a vector and memcpy issue I was having, was spending nearly 350ms just on reads for an image processing pipeline. With this simple modification I was able to drop that to 62.1ms on my machine Signed-off-by: evanokeeffe --- src/Tensor.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Tensor.cpp b/src/Tensor.cpp index a1eaccf2..dc2aad10 100644 --- a/src/Tensor.cpp +++ b/src/Tensor.cpp @@ -288,7 +288,10 @@ Tensor::getPrimaryBufferUsageFlags() { switch (this->mMemoryType) { case MemoryTypes::eDevice: - case MemoryTypes::eHost: + case TensorTypes::eHost: + return vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent; + break; case MemoryTypes::eDeviceAndHost: return vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | @@ -311,8 +314,8 @@ Tensor::getStagingBufferUsageFlags() { switch (this->mMemoryType) { case MemoryTypes::eDevice: - return vk::BufferUsageFlagBits::eTransferSrc | - vk::BufferUsageFlagBits::eTransferDst; + return vk::MemoryPropertyFlagBits::eHostVisible | + vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostCached; break; default: throw std::runtime_error("Kompute Tensor invalid tensor type"); From fe6dcd5d87db76d0b3a5a0e867e7e39ad04b6e24 Mon Sep 17 00:00:00 2001 From: evanokeeffe Date: Wed, 9 Sep 2026 21:53:30 +0100 Subject: [PATCH 2/3] Initial implementation of the Image sampler is ready, will send on for review soon Signed-off-by: evanokeeffe --- examples/image_sampler/CMakeLists.txt | 32 ++++ examples/image_sampler/README.md | 60 +++++++ examples/image_sampler/src/main.cpp | 73 ++++++++ examples/python_image_sampler/README.md | 28 +++ .../python_image_sampler/image_sampler.py | 83 +++++++++ python/src/main.cpp | 28 ++- src/Algorithm.cpp | 25 +-- src/Image.cpp | 74 +++++++- src/Tensor.cpp | 9 +- src/include/kompute/Image.hpp | 30 ++++ test/CMakeLists.txt | 1 + test/TestImageSampler.cpp | 165 ++++++++++++++++++ 12 files changed, 582 insertions(+), 26 deletions(-) create mode 100644 examples/image_sampler/CMakeLists.txt create mode 100644 examples/image_sampler/README.md create mode 100644 examples/image_sampler/src/main.cpp create mode 100644 examples/python_image_sampler/README.md create mode 100644 examples/python_image_sampler/image_sampler.py create mode 100644 test/TestImageSampler.cpp diff --git a/examples/image_sampler/CMakeLists.txt b/examples/image_sampler/CMakeLists.txt new file mode 100644 index 00000000..90dda469 --- /dev/null +++ b/examples/image_sampler/CMakeLists.txt @@ -0,0 +1,32 @@ +cmake_minimum_required(VERSION 3.20) +project(kompute_image_sampler) + +set(CMAKE_CXX_STANDARD 14) + +# Options +option(KOMPUTE_OPT_GIT_TAG "The tag of the repo to use for the example" v0.9.0) +option(KOMPUTE_OPT_FROM_SOURCE "Whether to build example from source or from git fetch repo" ON) + +if(KOMPUTE_OPT_FROM_SOURCE) + add_subdirectory(../../ ${CMAKE_CURRENT_BINARY_DIR}/kompute_build) +else() + include(FetchContent) + FetchContent_Declare(kompute GIT_REPOSITORY https://github.com/KomputeProject/kompute.git + GIT_TAG ${KOMPUTE_OPT_GIT_TAG}) + FetchContent_MakeAvailable(kompute) + include_directories(${kompute_SOURCE_DIR}/src/include) +endif() + +# Compiling shader +vulkan_compile_shader( + INFILE shader/upsample.comp + OUTFILE shader/upsample.hpp + NAMESPACE "shader") + +# Then add it to the library, so you can access it later in your code +add_library(shader INTERFACE "shader/upsample.hpp") +target_include_directories(shader INTERFACE $) + +# Setting up main example code +add_executable(kompute_image_sampler src/main.cpp) +target_link_libraries(kompute_image_sampler PRIVATE shader kompute::kompute) diff --git a/examples/image_sampler/README.md b/examples/image_sampler/README.md new file mode 100644 index 00000000..bf4c2ce4 --- /dev/null +++ b/examples/image_sampler/README.md @@ -0,0 +1,60 @@ +# Kompute Image Sampler Example + +This folder contains an end to end Kompute example that shows how to use +`kp::Image::createSampler()` to bind an image as a combined image sampler +(`sampler2D` in GLSL) instead of a plain storage image (`image2D`). + +The example uploads a tiny 4x4 checkerboard "image", attaches a sampler to +it, and dispatches a compute shader that fills a larger 16x16 output image +by sampling the small input with `texture()`. Because the input image has a +sampler attached, the GPU bilinearly filters between texels, producing a +smoothly interpolated (rather than blocky/nearest) upsample -- something +that isn't possible with `imageLoad`/`imageStore` on a plain storage image. + +This example is structured such that you will be able to extend it for your +project. It contains a CMake build configuration that can be used in your +production applications. + +## Building the example + +You will notice that it's a standalone project, so you can re-use it for +your application. It uses CMake's +[`fetch_content`](https://cmake.org/cmake/help/latest/module/FetchContent.html) +to consume Kompute as a dependency. To build you just need to run the CMake +command in this folder as follows: + +```bash +git clone https://github.com/KomputeProject/kompute.git +cd kompute/examples/image_sampler +mkdir build +cd build +cmake .. +cmake --build . +``` + +## Executing + +Form inside the `build/` directory run: + +### Linux + +```bash +./kompute_image_sampler +``` + +### Windows + +```bash +.\Debug\kompute_image_sampler.exe +``` + +## Pre-requisites + +In order to run this example, you will need the following dependencies: + +* REQUIRED + + The Vulkan SDK must be installed + +For the Vulkan SDK, the simplest way to install it is through +[their website](https://vulkan.lunarg.com/sdk/home). You just have to follow +the instructions for the relevant platform. diff --git a/examples/image_sampler/src/main.cpp b/examples/image_sampler/src/main.cpp new file mode 100644 index 00000000..17f79ca2 --- /dev/null +++ b/examples/image_sampler/src/main.cpp @@ -0,0 +1,73 @@ + +#include +#include +#include +#include + +#include +#include + +int +main() +{ + kp::Manager mgr; + + // A tiny 4x4 single-channel "image" (a coarse 2x2 checkerboard). + const uint32_t inWidth = 4; + const uint32_t inHeight = 4; + // clang-format off + std::vector inputData = { + 0.0f, 0.0f, 1.0f, 1.0f, + 0.0f, 0.0f, 1.0f, 1.0f, + 1.0f, 1.0f, 0.0f, 0.0f, + 1.0f, 1.0f, 0.0f, 0.0f, + }; + // clang-format on + + std::shared_ptr> inputImage = + mgr.image(inputData, inWidth, inHeight, 1); + + // Attaching a sampler switches this image's descriptor type from a + // storage image (`image2D`, raw imageLoad/imageStore) to a combined + // image sampler (`sampler2D`, hardware-filtered `texture()` reads). + // createSampler() with no arguments uses Kompute's default sampler + // (linear filtering, clamp-to-edge addressing). + inputImage->createSampler(); + + // A larger 16x16 output image that we fill by *sampling* (not just + // copying) the small input image, letting the GPU's bilinear filter + // smoothly interpolate between input texels. + const uint32_t outWidth = 16; + const uint32_t outHeight = 16; + std::vector outputData(outWidth * outHeight, 0.0f); + std::shared_ptr> outputImage = + mgr.image(outputData, outWidth, outHeight, 1); + + const std::vector> params = { inputImage, + outputImage }; + + const std::vector shader = std::vector( + shader::UPSAMPLE_COMP_SPV.begin(), shader::UPSAMPLE_COMP_SPV.end()); + + std::shared_ptr algo = mgr.algorithm( + params, shader, kp::Workgroup{ outWidth, outHeight, 1 }); + + mgr.sequence() + ->record(params) + ->record(algo) + ->record(params) + ->eval(); + + std::cout << "Upsampled output (" << outWidth << "x" << outHeight + << ") sampled from a " << inWidth << "x" << inHeight + << " source image:\n\n"; + + const std::vector result = outputImage->vector(); + for (uint32_t y = 0; y < outHeight; y++) { + for (uint32_t x = 0; x < outWidth; x++) { + std::cout << std::fixed << std::setprecision(2) + << result[y * outWidth + x] << " "; + } + std::cout << "\n"; + } +} diff --git a/examples/python_image_sampler/README.md b/examples/python_image_sampler/README.md new file mode 100644 index 00000000..0eb21f9b --- /dev/null +++ b/examples/python_image_sampler/README.md @@ -0,0 +1,28 @@ +# Python Image Sampler Example + +This demonstrates using `Image.create_sampler()` from Python to bind an image +as a combined image sampler (`sampler2D` in GLSL) instead of a plain storage +image (`image2D`). + +The script uploads a tiny 4x4 checkerboard "image", attaches a sampler to +it, and dispatches a compute shader that fills a larger 16x16 output image +by sampling the small input with `texture()`. Because the input image has a +sampler attached, the GPU bilinearly filters between texels, producing a +smoothly interpolated (rather than blocky/nearest) upsample -- something +that isn't possible with `imageLoad`/`imageStore` on a plain storage image. + +To run the example: + +```bash +python image_sampler.py +``` + +## Pre-requisites + +* REQUIRED + + The `kp` Python package (built with `KOMPUTE_OPT_BUILD_PYTHON=ON`, or + `pip install .` from the repo root) + + `numpy` + + `glslangValidator` available on your `PATH` (ships with the + [Vulkan SDK](https://vulkan.lunarg.com/sdk/home)), used here to + compile the inline GLSL shader at runtime diff --git a/examples/python_image_sampler/image_sampler.py b/examples/python_image_sampler/image_sampler.py new file mode 100644 index 00000000..d6a65f70 --- /dev/null +++ b/examples/python_image_sampler/image_sampler.py @@ -0,0 +1,83 @@ +import os + +import kp +import numpy as np + + +def compile_source(source): + open("tmp_kp_shader.comp", "w").write(source) + os.system("glslangValidator -V tmp_kp_shader.comp -o tmp_kp_shader.comp.spv") + return open("tmp_kp_shader.comp.spv", "rb").read() + + +def main(): + mgr = kp.Manager() + + # A tiny 4x4 single-channel "image" (a coarse 2x2 checkerboard). + in_width, in_height = 4, 4 + input_data = np.array( + [ + [0.0, 0.0, 1.0, 1.0], + [0.0, 0.0, 1.0, 1.0], + [1.0, 1.0, 0.0, 0.0], + [1.0, 1.0, 0.0, 0.0], + ], + dtype=np.float32, + ) + + input_image = mgr.image(input_data, in_width, in_height, 1) + + # Attaching a sampler switches this image's descriptor type from a + # storage image (`image2D`, raw imageLoad/imageStore) to a combined + # image sampler (`sampler2D`, hardware-filtered `texture()` reads). + # create_sampler() defaults to linear filtering with clamp-to-edge + # addressing. + input_image.create_sampler() + + # A larger 16x16 output image that we fill by *sampling* (not just + # copying) the small input image, letting the GPU's bilinear filter + # smoothly interpolate between input texels. + out_width, out_height = 16, 16 + output_data = np.zeros((out_height, out_width), dtype=np.float32) + output_image = mgr.image(output_data, out_width, out_height, 1) + + params = [input_image, output_image] + + shader = compile_source( + """ + #version 450 + + layout (local_size_x = 1, local_size_y = 1) in; + + layout(binding = 0) uniform sampler2D inputImg; + layout(binding = 1, r32f) writeonly uniform image2D outputImg; + + void main() { + ivec2 outCoord = ivec2(gl_GlobalInvocationID.xy); + ivec2 outSize = imageSize(outputImg); + vec2 uv = (vec2(outCoord) + 0.5) / vec2(outSize); + float value = texture(inputImg, uv).r; + imageStore(outputImg, outCoord, vec4(value, 0.0, 0.0, 0.0)); + } + """ + ) + + algo = mgr.algorithm(params, shader, (out_width, out_height, 1)) + + ( + mgr.sequence() + .record(kp.OpSyncDevice(params)) + .record(kp.OpAlgoDispatch(algo)) + .record(kp.OpSyncLocal(params)) + .eval() + ) + + print( + f"Upsampled output ({out_width}x{out_height}) sampled from a " + f"{in_width}x{in_height} source image:\n" + ) + print(np.round(output_image.data().reshape(out_height, out_width), 2)) + + +if __name__ == "__main__": + main() diff --git a/python/src/main.cpp b/python/src/main.cpp index 8bec2970..306fbbc3 100644 --- a/python/src/main.cpp +++ b/python/src/main.cpp @@ -274,7 +274,33 @@ PYBIND11_MODULE(kp, m) &kp::Memory::dataType), DOC(kp, Memory, dataType)) .def("is_init", &kp::Image::isInit, DOC(kp, Image, isInit)) - .def("destroy", &kp::Image::destroy, DOC(kp, Image, destroy)); + .def("destroy", &kp::Image::destroy, DOC(kp, Image, destroy)) + .def( + "create_sampler", + [](kp::Image& self, bool linear, bool repeat) { + vk::SamplerCreateInfo samplerInfo = + kp::Image::defaultSamplerCreateInfo(); + vk::Filter filter = + linear ? vk::Filter::eLinear : vk::Filter::eNearest; + samplerInfo.magFilter = filter; + samplerInfo.minFilter = filter; + vk::SamplerAddressMode addressMode = + repeat ? vk::SamplerAddressMode::eRepeat + : vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeU = addressMode; + samplerInfo.addressModeV = addressMode; + samplerInfo.addressModeW = addressMode; + self.createSampler(samplerInfo); + }, + "Attaches a Vulkan sampler to this image so it can be bound as a " + "combined image sampler (`sampler2D` in GLSL) instead of a storage " + "image (`image2D`), enabling hardware filtering/interpolation.", + py::arg("linear") = true, + py::arg("repeat") = false) + .def("has_sampler", + &kp::Image::hasSampler, + "Returns whether create_sampler() has been called on this " + "image."); py::class_>(m, "Sequence") .def( diff --git a/src/Algorithm.cpp b/src/Algorithm.cpp index 13f8301b..41f73a5d 100644 --- a/src/Algorithm.cpp +++ b/src/Algorithm.cpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 #include +#include #include "kompute/Algorithm.hpp" #include "kompute/Image.hpp" @@ -128,35 +129,23 @@ Algorithm::destroy() void Algorithm::createParameters() { - uint32_t numImages = 0; - uint32_t numTensors = 0; - KP_LOG_DEBUG("Kompute Algorithm createParameters started"); + std::map descriptorTypeCounts; + for (const std::shared_ptr& mem : this->mMemObjects) { - if (mem->getDescriptorType() == vk::DescriptorType::eStorageImage) { - numImages++; - } else { - numTensors++; - } + descriptorTypeCounts[mem->getDescriptorType()]++; } std::vector descriptorPoolSizes; - if (numTensors > 0) { + for (const auto& descriptorTypeCount : descriptorTypeCounts) { descriptorPoolSizes.push_back(vk::DescriptorPoolSize( - vk::DescriptorType::eStorageBuffer, - static_cast(numTensors) // Descriptor count + descriptorTypeCount.first, + descriptorTypeCount.second // Descriptor count )); } - if (numImages > 0) { - descriptorPoolSizes.push_back(vk::DescriptorPoolSize( - vk::DescriptorType::eStorageImage, - static_cast(numImages) // Descriptor count - )); - }; - vk::DescriptorPoolCreateInfo descriptorPoolInfo( vk::DescriptorPoolCreateFlags(), 1, // Max sets diff --git a/src/Image.cpp b/src/Image.cpp index a9344c3e..5a7645ff 100644 --- a/src/Image.cpp +++ b/src/Image.cpp @@ -416,6 +416,7 @@ Image::constructDescriptorImageInfo() descriptorInfo.imageView = *(mImageView.get()); descriptorInfo.imageLayout = this->mPrimaryImageLayout; + descriptorInfo.sampler = this->mSampler ? *this->mSampler : nullptr; return descriptorInfo; } @@ -431,11 +432,65 @@ Image::constructDescriptorSet(vk::DescriptorSet descriptorSet, uint32_t binding) binding, // Destination binding 0, // Destination array element 1, // Descriptor count - vk::DescriptorType::eStorageImage, + this->mDescriptorType, &mDescriptorImageInfo, nullptr); // Descriptor buffer info } +vk::SamplerCreateInfo +Image::defaultSamplerCreateInfo() +{ + vk::SamplerCreateInfo samplerInfo; + samplerInfo.magFilter = vk::Filter::eLinear; + samplerInfo.minFilter = vk::Filter::eLinear; + samplerInfo.addressModeU = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeV = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.addressModeW = vk::SamplerAddressMode::eClampToEdge; + samplerInfo.anisotropyEnable = VK_FALSE; + samplerInfo.maxAnisotropy = 1.0f; + samplerInfo.borderColor = vk::BorderColor::eIntOpaqueBlack; + samplerInfo.unnormalizedCoordinates = VK_FALSE; + samplerInfo.compareEnable = VK_FALSE; + samplerInfo.compareOp = vk::CompareOp::eAlways; + samplerInfo.mipmapMode = vk::SamplerMipmapMode::eNearest; + samplerInfo.mipLodBias = 0.0f; + samplerInfo.minLod = 0.0f; + samplerInfo.maxLod = 0.0f; + return samplerInfo; +} + +void +Image::createSampler(vk::SamplerCreateInfo samplerInfo) +{ + KP_LOG_DEBUG("Kompute Image creating sampler"); + + if (!this->mDevice) { + throw std::runtime_error("Kompute Image device is null"); + } + + if (this->mFreeSampler && this->mSampler) { + KP_LOG_DEBUG("Kompute Image destroying existing sampler before " + "creating a new one"); + this->mDevice->destroy( + *this->mSampler, + (vk::Optional)nullptr); + this->mSampler = nullptr; + this->mFreeSampler = false; + } + + this->mSampler = std::make_shared( + this->mDevice->createSampler(samplerInfo)); + this->mFreeSampler = true; + + this->mDescriptorType = vk::DescriptorType::eCombinedImageSampler; +} + +bool +Image::hasSampler() +{ + return this->mSampler != nullptr; +} + vk::ImageUsageFlags Image::getPrimaryImageUsageFlags() { @@ -444,11 +499,13 @@ Image::getPrimaryImageUsageFlags() case MemoryTypes::eHost: case MemoryTypes::eDeviceAndHost: return vk::ImageUsageFlagBits::eStorage | + vk::ImageUsageFlagBits::eSampled | vk::ImageUsageFlagBits::eTransferSrc | vk::ImageUsageFlagBits::eTransferDst; break; case MemoryTypes::eStorage: return vk::ImageUsageFlagBits::eStorage | + vk::ImageUsageFlagBits::eSampled | // You can still copy images to/from storage memory // so set the transfer usage flags here. vk::ImageUsageFlagBits::eTransferSrc | @@ -655,6 +712,21 @@ Image::destroy() this->mImageView = nullptr; } + if (this->mFreeSampler) { + if (!this->mSampler) { + KP_LOG_WARN("Kompose Image expected to destroy sampler " + "but got null sampler"); + } else { + KP_LOG_DEBUG("Kompose Image destroying sampler"); + this->mDevice->destroy( + *this->mSampler, + (vk::Optional)nullptr); + this->mSampler = nullptr; + this->mFreeSampler = false; + } + this->mDescriptorType = vk::DescriptorType::eStorageImage; + } + Memory::destroy(); KP_LOG_DEBUG("Kompute Image successful destroy()"); diff --git a/src/Tensor.cpp b/src/Tensor.cpp index dc2aad10..a1eaccf2 100644 --- a/src/Tensor.cpp +++ b/src/Tensor.cpp @@ -288,10 +288,7 @@ Tensor::getPrimaryBufferUsageFlags() { switch (this->mMemoryType) { case MemoryTypes::eDevice: - case TensorTypes::eHost: - return vk::MemoryPropertyFlagBits::eHostVisible | - vk::MemoryPropertyFlagBits::eHostCoherent; - break; + case MemoryTypes::eHost: case MemoryTypes::eDeviceAndHost: return vk::BufferUsageFlagBits::eStorageBuffer | vk::BufferUsageFlagBits::eTransferSrc | @@ -314,8 +311,8 @@ Tensor::getStagingBufferUsageFlags() { switch (this->mMemoryType) { case MemoryTypes::eDevice: - return vk::MemoryPropertyFlagBits::eHostVisible | - vk::MemoryPropertyFlagBits::eHostCoherent | vk::MemoryPropertyFlagBits::eHostCached; + return vk::BufferUsageFlagBits::eTransferSrc | + vk::BufferUsageFlagBits::eTransferDst; break; default: throw std::runtime_error("Kompute Tensor invalid tensor type"); diff --git a/src/include/kompute/Image.hpp b/src/include/kompute/Image.hpp index d254149e..c39745ce 100644 --- a/src/include/kompute/Image.hpp +++ b/src/include/kompute/Image.hpp @@ -309,6 +309,34 @@ class Image : public Memory */ uint32_t getNumChannels(); + /** + * Returns a default vk::SamplerCreateInfo suitable for sampling a + * single-mip 2D image: linear filtering, clamp-to-edge addressing, no + * anisotropy and no mipmapping. + * + * @return Default sampler creation parameters. + */ + static vk::SamplerCreateInfo defaultSamplerCreateInfo(); + + /** + * Creates a Vulkan sampler for this image and switches its descriptor + * type to vk::DescriptorType::eCombinedImageSampler so it can be bound + * to a `sampler2D` (rather than `image2D`) in a shader. Can be called + * again to replace an existing sampler with new parameters. + * + * @param samplerInfo Sampler creation parameters. Defaults to + * defaultSamplerCreateInfo(). + */ + void createSampler( + vk::SamplerCreateInfo samplerInfo = defaultSamplerCreateInfo()); + + /** + * Check whether this image currently has a sampler attached. + * + * @returns Boolean stating whether a sampler has been created. + */ + bool hasSampler(); + Type type() override { return Type::eImage; } protected: @@ -326,6 +354,8 @@ class Image : public Memory bool mFreePrimaryImage = false; std::shared_ptr mStagingImage; bool mFreeStagingImage = false; + std::shared_ptr mSampler = nullptr; + bool mFreeSampler = false; void allocateMemoryCreateGPUResources(); // Creates the vulkan image void createImage(std::shared_ptr image, diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 2974ee2b..40fc47f7 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -24,6 +24,7 @@ add_executable(kompute_tests TestAsyncOperations.cpp TestWorkgroup.cpp TestTensor.cpp TestImage.cpp + TestImageSampler.cpp TestOpImageCreate.cpp TestOpCopyTensor.cpp TestOpCopyTensorToImage.cpp diff --git a/test/TestImageSampler.cpp b/test/TestImageSampler.cpp new file mode 100644 index 00000000..743d2369 --- /dev/null +++ b/test/TestImageSampler.cpp @@ -0,0 +1,165 @@ +// SPDX-License-Identifier: Apache-2.0 + +#include "gtest/gtest.h" + +#include "kompute/Kompute.hpp" +#include "kompute/logger/Logger.hpp" + +#include "shaders/Utils.hpp" + +TEST(TestImageSampler, NoSamplerByDefault) +{ + kp::Manager mgr; + + std::shared_ptr> image = mgr.image({ 0, 1, 2, 3 }, 2, 2, 1); + + EXPECT_FALSE(image->hasSampler()); + EXPECT_EQ(image->getDescriptorType(), vk::DescriptorType::eStorageImage); +} + +TEST(TestImageSampler, CreateAndDestroySampler) +{ + kp::Manager mgr; + + std::shared_ptr> image = mgr.image({ 0, 1, 2, 3 }, 2, 2, 1); + + image->createSampler(); + + EXPECT_TRUE(image->hasSampler()); + EXPECT_EQ(image->getDescriptorType(), + vk::DescriptorType::eCombinedImageSampler); + + // Calling createSampler() again should replace the existing sampler + // rather than leaking or throwing. + vk::SamplerCreateInfo nearestSamplerInfo = + kp::Image::defaultSamplerCreateInfo(); + nearestSamplerInfo.magFilter = vk::Filter::eNearest; + nearestSamplerInfo.minFilter = vk::Filter::eNearest; + image->createSampler(nearestSamplerInfo); + + EXPECT_TRUE(image->hasSampler()); + EXPECT_EQ(image->getDescriptorType(), + vk::DescriptorType::eCombinedImageSampler); + + image->destroy(); + + EXPECT_FALSE(image->hasSampler()); + EXPECT_EQ(image->getDescriptorType(), vk::DescriptorType::eStorageImage); + EXPECT_FALSE(image->isInit()); +} + +TEST(TestImageSampler, DefaultSamplerCreateInfo) +{ + vk::SamplerCreateInfo samplerInfo = kp::Image::defaultSamplerCreateInfo(); + + EXPECT_EQ(samplerInfo.magFilter, vk::Filter::eLinear); + EXPECT_EQ(samplerInfo.minFilter, vk::Filter::eLinear); + EXPECT_EQ(samplerInfo.addressModeU, vk::SamplerAddressMode::eClampToEdge); + EXPECT_EQ(samplerInfo.addressModeV, vk::SamplerAddressMode::eClampToEdge); + EXPECT_EQ(samplerInfo.addressModeW, vk::SamplerAddressMode::eClampToEdge); + EXPECT_EQ(samplerInfo.anisotropyEnable, VK_FALSE); +} + +TEST(TestImageSampler, SampleWithBilinearFiltering) +{ + kp::Manager mgr; + + // A 2x1 texture with a left-to-right gradient from 0.0 to 1.0. + std::shared_ptr> inputImage = + mgr.image({ 0.0, 1.0 }, 2, 1, 1); + inputImage->createSampler(); + + // A 4x1 output that samples across the input via the combined image + // sampler, exercising the GPU's bilinear filter and clamp-to-edge + // addressing. + std::shared_ptr> outputImage = + mgr.image({ 0.0, 0.0, 0.0, 0.0 }, 4, 1, 1); + + const std::vector> params = { inputImage, + outputImage }; + + std::string shader = (R"( + #version 450 + + layout (local_size_x = 1, local_size_y = 1) in; + + layout(binding = 0) uniform sampler2D inputImg; + layout(binding = 1, r32f) writeonly uniform image2D outputImg; + + void main() { + ivec2 outCoord = ivec2(gl_GlobalInvocationID.xy); + ivec2 outSize = imageSize(outputImg); + vec2 uv = (vec2(outCoord) + 0.5) / vec2(outSize); + float value = texture(inputImg, uv).r; + imageStore(outputImg, outCoord, vec4(value, 0.0, 0.0, 0.0)); + } + )"); + + std::shared_ptr algo = mgr.algorithm( + params, compileSource(shader), kp::Workgroup{ 4, 1, 1 }); + + mgr.sequence() + ->eval(params) + ->eval(algo) + ->eval({ outputImage }); + + // Sample UVs land at 0.125, 0.375, 0.625, 0.875. With texel centers at + // 0.25 (value 0.0) and 0.75 (value 1.0) and clamp-to-edge addressing, + // the expected bilinearly filtered values are 0.0, 0.25, 0.75, 1.0. + const std::vector result = outputImage->vector(); + ASSERT_EQ(result.size(), 4u); + EXPECT_NEAR(result[0], 0.0, 1e-2); + EXPECT_NEAR(result[1], 0.25, 1e-2); + EXPECT_NEAR(result[2], 0.75, 1e-2); + EXPECT_NEAR(result[3], 1.0, 1e-2); +} + +TEST(TestImageSampler, MixedDescriptorTypesInSameAlgorithm) +{ + kp::Manager mgr; + + // A 1x1 sampled image acting as a "uniform" value. + std::shared_ptr> sampledImage = mgr.image({ 10.0 }, 1, 1, 1); + sampledImage->createSampler(); + + // A plain 2x1 storage image. + std::shared_ptr> storageImage = + mgr.image({ 1.0, 2.0 }, 2, 1, 1); + + // A tensor (storage buffer) output. + std::shared_ptr> tensorOut = + mgr.tensor({ 0.0, 0.0 }); + + const std::vector> params = { sampledImage, + storageImage, + tensorOut }; + + std::string shader = (R"( + #version 450 + + layout (local_size_x = 1) in; + + layout(binding = 0) uniform sampler2D sampledImg; + layout(binding = 1, r32f) uniform image2D storageImg; + layout(binding = 2) buffer bufOut { float o[]; }; + + void main() { + uint index = gl_GlobalInvocationID.x; + float sampledVal = texture(sampledImg, vec2(0.5, 0.5)).r; + float storageVal = imageLoad(storageImg, ivec2(int(index), 0)).r; + o[index] = sampledVal + storageVal; + } + )"); + + std::shared_ptr algo = mgr.algorithm( + params, compileSource(shader), kp::Workgroup{ 2, 1, 1 }); + + EXPECT_TRUE(algo->isInit()); + + mgr.sequence() + ->eval(params) + ->eval(algo) + ->eval({ tensorOut }); + + EXPECT_EQ(tensorOut->vector(), (std::vector{ 11.0, 12.0 })); +} From c7af17338f77ef2a775aac1e0a48269b94a67f81 Mon Sep 17 00:00:00 2001 From: evanokeeffe Date: Wed, 9 Sep 2026 22:09:37 +0100 Subject: [PATCH 3/3] fixing minor text and formatting Signed-off-by: evanokeeffe --- examples/image_sampler/CMakeLists.txt | 2 -- examples/image_sampler/README.md | 8 -------- examples/image_sampler/src/main.cpp | 4 +--- 3 files changed, 1 insertion(+), 13 deletions(-) diff --git a/examples/image_sampler/CMakeLists.txt b/examples/image_sampler/CMakeLists.txt index 90dda469..b8b6d9e2 100644 --- a/examples/image_sampler/CMakeLists.txt +++ b/examples/image_sampler/CMakeLists.txt @@ -23,10 +23,8 @@ vulkan_compile_shader( OUTFILE shader/upsample.hpp NAMESPACE "shader") -# Then add it to the library, so you can access it later in your code add_library(shader INTERFACE "shader/upsample.hpp") target_include_directories(shader INTERFACE $) -# Setting up main example code add_executable(kompute_image_sampler src/main.cpp) target_link_libraries(kompute_image_sampler PRIVATE shader kompute::kompute) diff --git a/examples/image_sampler/README.md b/examples/image_sampler/README.md index bf4c2ce4..74f783b6 100644 --- a/examples/image_sampler/README.md +++ b/examples/image_sampler/README.md @@ -15,14 +15,6 @@ This example is structured such that you will be able to extend it for your project. It contains a CMake build configuration that can be used in your production applications. -## Building the example - -You will notice that it's a standalone project, so you can re-use it for -your application. It uses CMake's -[`fetch_content`](https://cmake.org/cmake/help/latest/module/FetchContent.html) -to consume Kompute as a dependency. To build you just need to run the CMake -command in this folder as follows: - ```bash git clone https://github.com/KomputeProject/kompute.git cd kompute/examples/image_sampler diff --git a/examples/image_sampler/src/main.cpp b/examples/image_sampler/src/main.cpp index 17f79ca2..8c9c6a49 100644 --- a/examples/image_sampler/src/main.cpp +++ b/examples/image_sampler/src/main.cpp @@ -1,4 +1,3 @@ - #include #include #include @@ -7,8 +6,7 @@ #include #include -int -main() +int main() { kp::Manager mgr;