runcl

A minimal OpenCL harness for rendering images
git clone git://pollux.codes/git/runcl.git
Log | Files | Refs | README | LICENSE

commit aee366917834dc1b12fd69e09305036cabd0f05b
Author: Pollux <pollux@pollux.codes>
Date:   Sat, 18 Jan 2025 19:54:08 -0600

feat: initial commit

Signed-off-by: Pollux <pollux@pollux.codes>

Diffstat:
A.pre-commit-config.yaml | 17+++++++++++++++++
AMakefile | 77+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AREADME.md | 37+++++++++++++++++++++++++++++++++++++
Aexample.cl | 22++++++++++++++++++++++
Asrc/runcl.c | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 388 insertions(+), 0 deletions(-)

diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-case-conflict + - id: check-illegal-windows-names + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.6 + hooks: + - id: clang-format + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.1.0 + hooks: + - id: commitizen diff --git a/Makefile b/Makefile @@ -0,0 +1,77 @@ +# User-defined variables + +PREFIX ?= /usr/local +BINDIR ?= $(PREFIX)/bin +INCDIR ?= $(PREFIX)/include +BINMODE ?= 755 +LIBMODE ?= 644 +DOCMODE ?= 644 + +BINSUFFIX ?= + +# Programs used + +PKG_CONFIG := pkg-config +RM := rm -rf +MKDIR := mkdir +INSTALL := install + +# Other variables + +SRCDIR = src +OBJDIR = build +HDIR = include + +SRC_FILES = $(shell find $(SRCDIR) -name '*.c' -printf "%P ") + +LIBS = "OpenCL >= 3.0 libpng >= 1.6.44" +OBJS = $(addprefix $(OBJDIR)/,$(SRC_FILES:c=o)) + +# CFLAGS + +debug: CFLAGS = -g -O0 +debug: WFLAGS = -Werror +release: CFLAGS = -O2 +release: WFLAGS = -Wall -Wextra -Werror -Wpedantic + +RCFLAGS = -c $(CFLAGS) $(WFLAGS) -std=c99 $(shell pkg-config --cflags $(LIBS)) -I$(HDIR) +RLDFLAGS = $(LDFLAGS) $(shell pkg-config --libs $(LIBS)) + +# Build Info + +PROG_NAME = runcl +VERSION = $(shell git describe --tags --always 2>/dev/null) + +BUILD_TYPE = Release +debug: BUILD_TYPE = Debug + +# Preprocessor Flags + +CPPFLAGS = -DPROG_NAME=$(PROG_NAME) -DVERSION=$(VERSION) -DBUILD_TYPE=$(BUILD_TYPE) -DCFLAGS="$(RCFLAGS)" -DLDFLAGS="$(RLDFLAGS)" + +# Build Targets + +all: release +release: runcl +debug: runcl + +runcl: $(OBJS) + @printf "LD $@\n" + @$(CC) $(RLDFLAGS) -o runcl $(OBJS) + +$(OBJDIR)/%.o: $(SRCDIR)/%.c + @$(MKDIR) -p $(@D) + @printf "CC $@\n" + @$(CC) $(RCFLAGS) $(CPPFLAGS) -o $@ $^ + + +clean: + @printf "RM $(OBJDIR)\n" + @$(RM) $(OBJDIR) + @printf "RM runcl\n" + @$(RM) runcl + +install: + @$(INSTALL) -d $(DESTDIR)$(BINDIR)/ + @printf "INSTALL runcl -> $(DESTDIR)$(BINDIR)/runcl$(BINSUFFIX)\n" + @$(INSTALL) -s -m $(BINMODE) runcl $(DESTDIR)$(BINDIR)/runcl$(BINSUFFIX) diff --git a/README.md b/README.md @@ -0,0 +1,37 @@ +# runcl - A simple OpenCL harness for rendering images + +`runcl` is a minimal OpenCL harness allowing image generation on the GPU by +means of executing an OpenCL file to generate the image raster data. + +`runcl` depends on `libpng` and `libopencl`. + +`runcl` was designed to generate fractal art, a few examples of which can be +found at <https://pollux.codes/art>. + +## Installation + +```sh +make +sudo make install +``` + +## Usage + +The arguments of `runcl` are mostly self-explanatory: +```sh +runcl -p <platform-index> -w <image-width> -h <image-height> input-opencl-file output-png-file +``` + +Use `clinfo` to see platform information, by default the platform with index 0 +is chosen. + +## CL File Requirements + +In order for runcl to work, the OpenCL file used must meet the following +requirements: + +- Kernel named 'render' +- Arguments 'const int width', 'const int height', and '__global float *out' + +See the `example.cl` file for a demonstration of these requirements, as well as +the suggested way of using the arguments. diff --git a/example.cl b/example.cl @@ -0,0 +1,22 @@ +// This is the main function of the program. +// Runcl passes the following arguments to the program. +// - width: the width of the image in pixels +// - height: the height of the image in pixels +// - *out: the output of the program, formatted as an 3xwxh array of normalized rgm pixel colors +kernel void render(const int width, const int height, __global float *out) +{ + // this gets the id of the particular instance + const int i = get_global_id(0); + + if ( i < width * height ) + { + // this converts the id into normalized image coordinates + float x = (float)(i % width) / width; + float y = (float)(i / width) / height; + + // output the rgb color to the out array + out[3*i] = x; + out[3*i+1] = x; + out[3*i+2] = x; + } +} diff --git a/src/runcl.c b/src/runcl.c @@ -0,0 +1,235 @@ + +#define CL_TARGET_OPENCL_VERSION 300 +#define USAGE \ + "usage: runcl -p <platform-index> -w <image-width> -h <image-height> " \ + "input-opencl-file output-png-file" + +#define STR_LITERAL(x) #x +#define STRINGIFY(x) STR_LITERAL(x) + +#include <CL/cl.h> +#include <getopt.h> +#include <png.h> +#include <stdio.h> +#include <unistd.h> + +void print_help() { printf(USAGE "\n"); } + +void print_version() { + printf(STRINGIFY(PROG_NAME) " " STRINGIFY(VERSION) "\n"); + printf("Build: " STRINGIFY(BUILD_TYPE) "\n"); + printf("CFLAGS: " STRINGIFY(CFLAGS) "\n"); + printf("LDFLAGS: " STRINGIFY(LDFLAGS) "\n"); +} + +int main(int argc, char **argv) { + + char *source_file = NULL; + char *out_file = NULL; + + int img_width = 640; + int img_height = 480; + int platform_index = 0; + + int c; + + opterr = 0; + + int data_size_out = sizeof(float) * img_width * img_height * 3; + float *data_out = (float *)malloc(data_size_out); + + cl_platform_id platform_id = NULL; + + cl_device_id device_id = NULL; + + cl_mem out_buff; + + cl_context context = NULL; + cl_command_queue command_queue = NULL; + cl_kernel kernel = NULL; + cl_program program = NULL; + + char *source_str = NULL; + size_t source_size = 0; + + FILE *fp; + + cl_uint platform_count = 256; + cl_platform_id *platforms = malloc(platform_count * sizeof(cl_platform_id)); + + cl_int err; + + size_t g_work_size, l_work_size; + + char name[100]; + size_t name_size; + + png_structp png_ptr; + png_infop info_ptr; + png_bytep row; + row = (png_bytep)malloc(3 * img_width * sizeof(png_byte)); + + while ((c = getopt(argc, argv, "p:w:h:Hv")) != -1) { + switch (c) { + case 'p': + platform_index = atoi(optarg); + break; + case 'w': + img_width = atoi(optarg); + break; + case 'h': + img_height = atoi(optarg); + break; + case 'H': + print_help(); + exit(0); + break; + case 'v': + print_version(); + exit(0); + break; + case '?': + fprintf(stderr, "Error parsing command-line arguments...\n"); + return 1; + } + } + + if (argc < optind + 2) { + fprintf(stderr, "Error parsing command-line arguments...\n"); + return 1; + } + + source_file = argv[optind]; + out_file = argv[optind + 1]; + + // Load Program Source + + fp = fopen(source_file, "r"); + if (!fp) { + printf("Cannot open file: %s\n", source_file); + goto error; + } + + source_str = (char *)malloc(1048576); + source_size = fread(source_str, 1, 1048576, fp); + fclose(fp); + + // Define Vars + + // Get Platform & Device + clGetPlatformIDs(0, NULL, &platform_count); + + clGetPlatformIDs(platform_count, platforms, NULL); + + platform_id = platforms[platform_index]; + + clGetDeviceIDs(platform_id, CL_DEVICE_TYPE_GPU, 1, &device_id, NULL); + + clGetPlatformInfo(platform_id, CL_PLATFORM_NAME, 100, name, &name_size); + + printf("Platform in Use: %s\n", name); + + // Create Context & Queue + context = clCreateContext(NULL, 1, &device_id, NULL, NULL, NULL); + command_queue = + clCreateCommandQueueWithProperties(context, device_id, 0, NULL); + + // Create Buffers + out_buff = + clCreateBuffer(context, CL_MEM_WRITE_ONLY, data_size_out, NULL, NULL); + + program = clCreateProgramWithSource(context, 1, (const char **)&source_str, + (const size_t *)&source_size, NULL); + + err = clBuildProgram(program, 1, &device_id, NULL, NULL, NULL); + + if (err != CL_SUCCESS) { + printf("Failed to build program!\n"); + char build_log[16348]; + clGetProgramBuildInfo(program, device_id, CL_PROGRAM_BUILD_LOG, + sizeof(build_log), build_log, NULL); + printf("Build Error: %s\n", build_log); + goto error; + } + + kernel = clCreateKernel(program, "render", &err); + + if (err != CL_SUCCESS) { + printf("Error creating kernel: %d\n", (int)err); + goto error; + } + + clSetKernelArg(kernel, 0, sizeof(cl_int), (void *)&img_width); + clSetKernelArg(kernel, 1, sizeof(cl_int), (void *)&img_height); + clSetKernelArg(kernel, 2, sizeof(cl_mem), (void *)&out_buff); + + l_work_size = 128; + g_work_size = (img_width * img_height / l_work_size + 1) * l_work_size; + + err = clEnqueueNDRangeKernel(command_queue, kernel, 1, NULL, &g_work_size, + &l_work_size, 0, NULL, NULL); + + if (err != CL_SUCCESS) { + printf("Error executing kernel: %d\n", (int)err); + goto error; + } + + err = clEnqueueReadBuffer(command_queue, out_buff, CL_TRUE, 0, data_size_out, + (void *)data_out, 0, NULL, NULL); + + if (err != CL_SUCCESS) { + printf("Error reading result: %d\n", (int)err); + goto error; + } + + // Write PNG + fp = fopen(out_file, "wb"); + + if (!fp) { + printf("Couldn't open output file\n"); + goto error; + } + + png_ptr = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + + info_ptr = png_create_info_struct(png_ptr); + + png_init_io(png_ptr, fp); + + png_set_IHDR(png_ptr, info_ptr, img_width, img_height, 8, PNG_COLOR_TYPE_RGB, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_BASE, + PNG_FILTER_TYPE_BASE); + + png_write_info(png_ptr, info_ptr); + + int x, y; + for (y = 0; y < img_height; y++) { + for (x = 0; x < img_width; x++) { + row[x * 3 + 0] = (int)(255 * data_out[3 * (y * img_width + x) + 0]); + row[x * 3 + 1] = (int)(255 * data_out[3 * (y * img_width + x) + 1]); + row[x * 3 + 2] = (int)(255 * data_out[3 * (y * img_width + x) + 2]); + } + png_write_row(png_ptr, row); + } + + png_write_end(png_ptr, NULL); + + fclose(fp); + +error: + + clFlush(command_queue); + clFinish(command_queue); + + clReleaseMemObject(out_buff); + + clReleaseCommandQueue(command_queue); + clReleaseContext(context); + + free(platforms); + free(source_str); + free(data_out); + free(row); + + return 0; +}