diff --git a/.gitignore b/.gitignore index 11c7643..1af9604 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ compile_commands.json CTestTestfile.cmake _deps CMakeUserPresets.json + +# Visual Studio +.vs/ +*.user diff --git a/App/CMakeLists.txt b/App/CMakeLists.txt new file mode 100644 index 0000000..c3a7d17 --- /dev/null +++ b/App/CMakeLists.txt @@ -0,0 +1,18 @@ + +# App + +set(SOURCE_DIR "Source") + +set(SOURCES + Source/Main.cpp + Source/Shader.h + Source/Shader.cpp + Source/Renderer.h + Source/Renderer.cpp +) + +# Add the executable target +add_executable(App ${SOURCES}) +target_link_libraries(App glfw) +target_link_libraries(App glad) +target_link_libraries(App glm) \ No newline at end of file diff --git a/App/Shaders/Compute.glsl b/App/Shaders/Compute.glsl new file mode 100644 index 0000000..50a1882 --- /dev/null +++ b/App/Shaders/Compute.glsl @@ -0,0 +1,32 @@ +#version 460 core + +layout(rgba32f, binding = 0) uniform writeonly image2D outputImage; + +layout(local_size_x = 16, local_size_y = 16) in; +void main() +{ + ivec2 pixelCoord = ivec2(gl_GlobalInvocationID.xy); + + if (pixelCoord.x >= imageSize(outputImage).x || pixelCoord.y >= imageSize(outputImage).y) + return; + + ivec2 texSize = imageSize(outputImage); + vec2 fTexSize = vec2(texSize); + vec2 normalizedCoord = vec2(pixelCoord) / vec2(texSize); + + vec4 O = vec4(0, 0, 0, 1); + vec2 I = vec2(pixelCoord); + + float iTime = 2.2; + float i = 0.0, t=iTime; + O *= i; + for(vec2 a=fTexSize.xy, p=(I+I-a)/a.y; i++<20.; + O += (cos(sin(i*.2+t)*vec4(0,4,3,1))+2.) + /(i/1e3+abs(length(a-.5*min(a+a.yx,.1))-.05))) + a.x = abs(a = (fract(.2*t+.3*p*i*mat2(cos(cos(.2*t+.2*i)+vec4(0,11,33,0))))-.5)).x; + + O = tanh(O*O/2e5); + + vec4 color = vec4(normalizedCoord, 0.0, 1.0); + imageStore(outputImage, pixelCoord, O); +} \ No newline at end of file diff --git a/App/Source/Main.cpp b/App/Source/Main.cpp new file mode 100644 index 0000000..da8d929 --- /dev/null +++ b/App/Source/Main.cpp @@ -0,0 +1,102 @@ +#include + +#include + +#include "Shader.h" +#include "Renderer.h" + +static uint32_t s_ComputeShader = -1; +static const std::filesystem::path s_ComputeShaderPath = "Shaders/Compute.glsl"; + +static void ErrorCallback(int error, const char* description) +{ + std::cerr << "Error: " << description << std::endl; +} + +static void KeyCallback(GLFWwindow* window, int key, int scancode, int action, int mods) +{ + if (key == GLFW_KEY_ESCAPE && action == GLFW_PRESS) + glfwSetWindowShouldClose(window, GLFW_TRUE); + + if (key == GLFW_KEY_R) + s_ComputeShader = ReloadComputeShader(s_ComputeShader, s_ComputeShaderPath); +} + +int main() +{ + glfwSetErrorCallback(ErrorCallback); + + if (!glfwInit()) + exit(EXIT_FAILURE); + + glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4); + glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 6); + + int width = 1280; + int height = 720; + + GLFWwindow* window = glfwCreateWindow(width, height, "Compute", NULL, NULL); + if (!window) + { + glfwTerminate(); + exit(EXIT_FAILURE); + } + + glfwSetKeyCallback(window, KeyCallback); + + glfwMakeContextCurrent(window); + gladLoadGL(glfwGetProcAddress); + glfwSwapInterval(1); + + s_ComputeShader = CreateComputeShader(s_ComputeShaderPath); + if (s_ComputeShader == -1) + { + std::cerr << "Compute shader failed\n"; + return -1; + } + + Texture computeShaderTexture = CreateTexture(width, height); + Framebuffer fb = CreateFramebufferWithTexture(computeShaderTexture); + + while (!glfwWindowShouldClose(window)) + { + glfwGetFramebufferSize(window, &width, &height); + + // Resize texture + if (width != computeShaderTexture.Width || height != computeShaderTexture.Height) + { + glDeleteTextures(1, &computeShaderTexture.Handle); + computeShaderTexture = CreateTexture(width, height); + AttachTextureToFramebuffer(fb, computeShaderTexture); + } + + // Compute + { + glUseProgram(s_ComputeShader); + glBindImageTexture(0, fb.ColorAttachment.Handle, 0, GL_FALSE, 0, GL_WRITE_ONLY, GL_RGBA32F); + + const GLuint workGroupSizeX = 16; + const GLuint workGroupSizeY = 16; + + GLuint numGroupsX = (width + workGroupSizeX - 1) / workGroupSizeX; + GLuint numGroupsY = (height + workGroupSizeY - 1) / workGroupSizeY; + + glDispatchCompute(numGroupsX, numGroupsY, 1); + + // Ensure all writes to the image are complete + glMemoryBarrier(GL_SHADER_IMAGE_ACCESS_BARRIER_BIT); + } + + // Blit + { + BlitFramebufferToSwapchain(fb); + } + + glfwSwapBuffers(window); + glfwPollEvents(); + } + + glfwDestroyWindow(window); + + glfwTerminate(); +} diff --git a/App/Source/Renderer.cpp b/App/Source/Renderer.cpp new file mode 100644 index 0000000..8720345 --- /dev/null +++ b/App/Source/Renderer.cpp @@ -0,0 +1,61 @@ +#include "Renderer.h" + +#include + +Texture CreateTexture(int width, int height) +{ + Texture result; + result.Width = width; + result.Height = height; + + glCreateTextures(GL_TEXTURE_2D, 1, &result.Handle); + + glTextureStorage2D(result.Handle, 1, GL_RGBA32F, width, height); + + glTextureParameteri(result.Handle, GL_TEXTURE_MIN_FILTER, GL_NEAREST); + glTextureParameteri(result.Handle, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTextureParameteri(result.Handle, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + + return result; +} + +Framebuffer CreateFramebufferWithTexture(const Texture texture) +{ + Framebuffer result; + + glCreateFramebuffers(1, &result.Handle); + + if (!AttachTextureToFramebuffer(result, texture)) + { + glDeleteFramebuffers(1, &result.Handle); + return {}; + } + + return result; +} + +bool AttachTextureToFramebuffer(Framebuffer& framebuffer, const Texture texture) +{ + glNamedFramebufferTexture(framebuffer.Handle, GL_COLOR_ATTACHMENT0, texture.Handle, 0); + + if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) + { + std::cerr << "Framebuffer is not complete!" << std::endl; + return false; + } + + framebuffer.ColorAttachment = texture; + return true; +} + +void BlitFramebufferToSwapchain(const Framebuffer framebuffer) +{ + glBindFramebuffer(GL_READ_FRAMEBUFFER, framebuffer.Handle); + glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0); // swapchain + + glBlitFramebuffer(0, 0, framebuffer.ColorAttachment.Width, framebuffer.ColorAttachment.Height, // Source rect + 0, 0, framebuffer.ColorAttachment.Width, framebuffer.ColorAttachment.Height, // Destination rect + GL_COLOR_BUFFER_BIT, GL_NEAREST); +} \ No newline at end of file diff --git a/App/Source/Renderer.h b/App/Source/Renderer.h new file mode 100644 index 0000000..83214d5 --- /dev/null +++ b/App/Source/Renderer.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#define GLFW_INCLUDE_NONE +#include + +struct Texture +{ + GLuint Handle = 0; + uint32_t Width = 0; + uint32_t Height = 0; +}; + +struct Framebuffer +{ + GLuint Handle = 0; + Texture ColorAttachment; +}; + +Texture CreateTexture(int width, int height); +Framebuffer CreateFramebufferWithTexture(const Texture texture); +bool AttachTextureToFramebuffer(Framebuffer& framebuffer, const Texture texture); +void BlitFramebufferToSwapchain(const Framebuffer framebuffer); \ No newline at end of file diff --git a/App/Source/Shader.cpp b/App/Source/Shader.cpp new file mode 100644 index 0000000..fb04437 --- /dev/null +++ b/App/Source/Shader.cpp @@ -0,0 +1,81 @@ +#include "Shader.h" + +#include +#include + +#include + +uint32_t CreateComputeShader(const std::filesystem::path& path) +{ + std::ifstream file(path); + + if (!file.is_open()) + { + std::cerr << "Failed to open file: " << path.string() << std::endl; + return -1; + } + + std::ostringstream contentStream; + contentStream << file.rdbuf(); + std::string shaderSource = contentStream.str(); + + GLuint shaderHandle = glCreateShader(GL_COMPUTE_SHADER); + + const GLchar* source = (const GLchar*)shaderSource.c_str(); + glShaderSource(shaderHandle, 1, &source, 0); + + glCompileShader(shaderHandle); + + GLint isCompiled = 0; + glGetShaderiv(shaderHandle, GL_COMPILE_STATUS, &isCompiled); + if (isCompiled == GL_FALSE) + { + GLint maxLength = 0; + glGetShaderiv(shaderHandle, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog(maxLength); + glGetShaderInfoLog(shaderHandle, maxLength, &maxLength, &infoLog[0]); + + std::cerr << infoLog.data() << std::endl; + + glDeleteShader(shaderHandle); + return -1; + } + + GLuint program = glCreateProgram(); + glAttachShader(program, shaderHandle); + glLinkProgram(program); + + GLint isLinked = 0; + glGetProgramiv(program, GL_LINK_STATUS, (int*)&isLinked); + if (isLinked == GL_FALSE) + { + GLint maxLength = 0; + glGetProgramiv(program, GL_INFO_LOG_LENGTH, &maxLength); + + std::vector infoLog(maxLength); + glGetProgramInfoLog(program, maxLength, &maxLength, &infoLog[0]); + + std::cerr << infoLog.data() << std::endl; + + glDeleteProgram(program); + glDeleteShader(shaderHandle); + + return -1; + } + + glDetachShader(program, shaderHandle); + return program; +} + +uint32_t ReloadComputeShader(uint32_t shaderHandle, const std::filesystem::path& path) +{ + uint32_t newShaderHandle = CreateComputeShader(path); + + // Return old shader if compilation failed + if (newShaderHandle == -1) + return shaderHandle; + + glDeleteProgram(shaderHandle); + return newShaderHandle; +} diff --git a/App/Source/Shader.h b/App/Source/Shader.h new file mode 100644 index 0000000..d628203 --- /dev/null +++ b/App/Source/Shader.h @@ -0,0 +1,6 @@ +#pragma once + +#include + +uint32_t CreateComputeShader(const std::filesystem::path& path); +uint32_t ReloadComputeShader(uint32_t shaderHandle, const std::filesystem::path& path); \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..66c16e6 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,69 @@ +cmake_minimum_required(VERSION 3.28) +include(FetchContent) + +project(App) + +set(CMAKE_CXX_STANDARD 20) + +# +# Dependencies +# + +# GLFW +find_package(glfw3 3.4 QUIET) +if (NOT glfw3_FOUND) + FetchContent_Declare( + glfw3 + DOWNLOAD_EXTRACT_TIMESTAMP OFF + URL https://github.com/glfw/glfw/releases/download/3.4/glfw-3.4.zip + ) + FetchContent_GetProperties(glfw3) + if (NOT glfw3_POPULATED) + set(FETCHCONTENT_QUIET NO) + FetchContent_Populate(glfw3) + add_subdirectory(${glfw3_SOURCE_DIR} ${glfw3_BINARY_DIR}) + endif() +endif() + +# OpenGL +find_package(OpenGL REQUIRED) + +# GLAD +FetchContent_Declare( + glad + DOWNLOAD_EXTRACT_TIMESTAMP OFF + URL https://github.com/Dav1dde/glad/archive/refs/tags/v2.0.8.zip +) + +FetchContent_GetProperties(glad) +if(NOT glad_POPULATED) + set(FETCHCONTENT_QUIET NO) + FetchContent_MakeAvailable(glad) + + add_subdirectory("${glad_SOURCE_DIR}/cmake" glad_cmake) + glad_add_library(glad REPRODUCIBLE EXCLUDE_FROM_ALL LOADER API gl:core=4.6) +endif() +set_target_properties(glad PROPERTIES FOLDER "Dependencies") + +# GLM +find_package(glm 1.0.1 QUIET) +if (NOT glm_FOUND) + FetchContent_Declare( + glm + DOWNLOAD_EXTRACT_TIMESTAMP OFF + URL https://github.com/g-truc/glm/archive/refs/tags/1.0.1.zip + ) + FetchContent_GetProperties(glm) + if (NOT glm_POPULATED) + set(FETCHCONTENT_QUIET NO) + FetchContent_Populate(glm) + add_subdirectory(${glm_SOURCE_DIR} ${glm_BINARY_DIR}) + endif() +endif() +set_target_properties(glm PROPERTIES FOLDER "Dependencies") + +# +# Projects +# + +add_subdirectory(App) \ No newline at end of file diff --git a/README.md b/README.md index 345a29a..f73b68d 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,18 @@ -# GPUCompute -Simple app to run compute shaders using OpenGL +# GPU Compute +Simple app to run compute shaders using OpenGL. + +## Building +Just run CMake and build. Here's an example: +``` +mkdir build +cd build +cmake .. +``` + +Tested on Windows 11 using Visual Studio 2022, more testing to follow. + +## Third-Party Dependencies +Uses the following third-party dependencies: +- [GLFW 3.4](https://github.com/glfw/glfw) +- [Glad](https://github.com/Dav1dde/glad) +- [glm](https://github.com/g-truc/glm) \ No newline at end of file