C++ / SDL로 망델브로 집합 그려보기 – 7

SDL로 망델브로 집합 그려보기 시리즈

7-1. CUDA GPU 가속기능 추가

NVIDIA GPU가 내장된 컴퓨터에서만 구동 가능한 점 미리 알려드립니다. 모든 GPU에서 돌아가도록 하려면openCL을 사용하거나 vulkan또는 directX의 컴퓨팅 쉐이더를 이용해 구현해야 합니다.

7-1-1. CUDA 설치

CUDA는 vcpkg가 아닌 수동으로 설치하도록 하겠습니다. NVIDIA의 웹페이지에서 CUDA toolkit을 다운로드하고 설치합니다. 그리고 비주얼 스튜디오로 넘어와서 프로젝트 설정의 추가 포함 디렉터리에 $(CUDA_PATH)\include를 추가합니다. 그리고 링커-일반의 추가 라이브러리 디렉터리에 $(CUDA_PATH)\lib\x64를 추가합니다. 마지막으로 링커-입력의 추가 종속성에 cudart.lib를 추가합니다.

비주얼 스튜디오의 솔루션 탐색기에서 프로젝트를 우클릭하고 빌드 종속성을 클릭합니다. 아래 이미지와 같이 CUDA를 선택해야 합니다.

7-1-2. MandelbrotCUDA 클래스

mandelbrot_cuda.h와 mandelbrot_cuda.cu 파일을 추가해 줍니다. CUDA가 컴파일하는 소스파일은 확장자를 .cpp가 아닌 .cu를 써줘야 합니다. 아래 이미지 처럼 솔루션 탐색기에서 mandelbrot_cuda.cu를 우클릭하고 속성의 item type을 CUDA C/C++로 선택해 줍니다(위에서 빌드종속성에 CUDA를 추가해야 해당 item type이 뜹니다).

CUDA로 구현하면서 수정해야 할 함수가 있기 때문에 Mandelbrot클래스의 다음 메서드들 앞에 virtual 키워드를 붙여줍니다.

mandelbrot.h
	virtual void resize();

	inline std::complex<real_t> getPosition() const { return { pos_x, pos_y }; }
	void setPosition(real_t x, real_t y);
	virtual void move(int32_t rel_px, int32_t rel_py);
	...
	virtual void update(bool rerender_all = true, bool clear_surface = true);

MandelbrotCUDA의 멤버변수를 살펴봅시다. device_surface는 디바이스(GPU)에 복사된 surface의 메모리 주소입니다. 따라서 호스트(CPU)에서 이 포인터에 접근하면 오류가 발생합니다. 마찬가지로 device_pixel_info도 디바이스에 복사된 render_info->pixels의 메모리 주소입니다.

block_size는 GPU스레드의 실행단위 크기입니다. GPU는 하나의 명령에 여러 스레드가 여러 데이터를 동시에 처리하는 SIMD장치입니다. 예를 들어 block_size가 4로 설정되면 이 예제에선 2D 이미지를 처리하므로, 4×4=16개의 스레드가 동시에 실행됩니다. 물론 블록이라는 실행단위에서 16개의 스레드가 동시에 실행된다는 의미이고 실제로는 GPU 장치에 따라 다르지만 수천개의 스레드가 동시에 실행됩니다.

streams는 CUDA의 동기화 장치입니다. 스트림을 통해서만 비동기적으로 GPU에 명령을 전달하고 메모리 전송과 연산을 동시에 수행할 수 있습니다. streams[0]은 전송, streams[1]은 커널 실행에 사용됩니다.

mandelbrot_cuda.h
#pragma once

#include "mandelbrot.h"

struct CUstream_st;
typedef struct CUstream_st* cudaStream_t;

class MandelbrotCUDA : public Mandelbrot
{
public:
	using real_t = Mandelbrot::real_t;

	using Mandelbrot::Mandelbrot;

	MandelbrotCUDA(SDL_Renderer* renderer);
	~MandelbrotCUDA() override;

	inline uint32_t getBlockSize() const { return block_size; }
	inline void setBlockSize(uint32_t size) { block_size = size; }

	void draw() override;
	void stop() override;
	void wait() override;
	void resize() override;

private:
	void move(int32_t rel_px, int32_t rel_py) override;
	void drawSurface() override;
	void update(bool rerender_all = true, bool clear_surface = true) override;

	uint32_t*  device_surface;
	PixelInfo* device_pixel_info;

	uint32_t block_size;

	cudaStream_t streams[2];
};
C++

아래 코드에 대해 설명하겠습니다. 기본적인 CUDA 문법이 사용되는데 __device__가 앞에 붙은 함수는 디바이스에서 실행되며 디바이스만이 호출할 수 있는 함수 __global__이 앞에 붙은 함수는 디바이스에서 실행되며 호스트도 호출할 수 있는 함수입니다. 13, 14번 줄에서 미리 만들어 놓은 함수를 __device__키워드로 다시 선언하여 디바이스 코드에서도 호출할 수 있도록 해줍니다.

28번 줄에서 사용되는 __constant__는 GPU의 상수 메모리에 데이터를 저장할 수 있게 해줍니다. 상수 메모리는 용량이 적은 대신 매우 빠른 메모리 입니다. 따라서 자주 변하지 않으면서 많이 접근해야 할 데이터를 여기 저장합니다. 일반적인 메모리는 복사에 cudaMemcpy()를 사용하지만, 이 메모리는 cudaMemcpyToSymbol()을 사용해야 합니다. 93, 127, 129, 204번 줄에 사용된 이 함수에서 인텔리센스 오류가 나타납니다. 하지만 CUDA 컴파일러가 알아서 잘 처리해주기 때문에 실제론 빌드가 잘 됩니다.

cudaHostRegister()는 페이징되지 않는 메모리로 할당된 메모리를 설정하는 함수입니다. 자세하게 설명하자면 복잡하지만, 간단하게 역할만 설명하자면 CPU-GPU간 메모리 전송속도를 올리기위해 사용합니다.

전체 코드를 최대한 줄여서 작성하다 보니 161번 줄 아래의 함수에 심각하게 비효율적인 부분이 있습니다. GPU 메모리에 있는 렌더링 데이터와 surface정보를 CPU로 읽어와서 변형 후 다시 GPU로 전송하는 부분인데 GPU에서 이미지 픽셀을 이동시키는 함수만 만든다면 이를 이렇게 하지 않고 바로 GPU상에서 바꿀 수 있습니다. 그렇게 하려면 아래 코드가 심히 길어지기 때문에 이건 지금까지 잘 따라와주신 분들의 숙제로 남기겠습니다…

185번 줄에 GPU 코드를 실행시키는 부분이 있습니다. mandelbrot_kernel<<<? ? ?>>>같은 괴상한 문법이 등장하는데 마찬가지로 CUDA 전용 확장문법으로 인텔리센스 오류는 뜨지만 빌드는 잘 됩니다.

CUDA를 사용하면서 단점이 하나 생깁니다. 바로 비동기 렌더링이 제대로 작동하지 않는다는 점 입니다. CUDA에선 실행중인 커널을 종료시키기 위한 방법이 없습니다. 그래도 최대한 가능하도록 만드는 방법은 전역 변수를 하나 둬서 GPU의 스레드가 실행 될 때 그 변수를 보고 종료 시그널을 읽는다면 바로 리턴하는 것 입니다. 하지만 이 방법은 결국 모든 스레드에 대해 한번은 실행되어야 하므로 지연이 생깁니다. 따라서 비동기 렌더링을 켜도 버벅거리는 현상이 나타납니다.

mandelbrot_cuda.cu
#include "mandelbrot_cuda.h"

#include <cuda_runtime.h>
#include <device_launch_parameters.h>
#include "color.h"

using namespace std;

using real_t    = Mandelbrot::real_t;
using PixelInfo = Mandelbrot::PixelInfo;

template <class T>
__device__ inline uint32_t mandelbrot(T& cx, T& cy, uint32_t max_iter);
__device__ inline uint32_t lerp_color(uint32_t a, uint32_t b, double t);

struct Constants {
	int      width;
	int      height;
	uint32_t iter;

	uint32_t color_idx;
	real_t   color_scale;
	bool     color_smooth;

	bool     stop_all;
};

extern "C" {
	__constant__ uint32_t cuda_colormap[6][256];
	__constant__  Constants params;
}

template <class T>
__device__ uint32_t get_color_idx(T iter) {
	return (uint32_t)(params.color_scale * 256 * iter / params.iter) % 256;
}

__global__ void mandelbrot_kernel(
	uint32_t*  device_surface, 
	PixelInfo* device_pixel_info,
	real_t     min_x,
	real_t     max_y,
	real_t     dp
) {
	if (params.stop_all) return;

	int w = blockDim.x * blockIdx.x + threadIdx.x;
	int h = blockDim.x * blockIdx.y + threadIdx.y;

	if (params.width <= w || params.height <= h) return;

	auto pixel_off  = h * params.width + w;
	auto& info      = *(device_pixel_info + pixel_off);

	if (info.rendered) return;
 
	real_t cx = min_x + dp * (w + 0.5f);
	real_t cy = max_y - dp * (h + 0.5f);

	auto iterated = mandelbrot<real_t>(cx, cy, params.iter);

	uint32_t& pixel = *(device_surface + pixel_off);

	if (iterated == params.iter) {
		pixel = 0xff000000;
	} else if (!params.color_smooth) {
		pixel = cuda_colormap[params.color_idx][get_color_idx(iterated)];
	} else {
		double log_zn  = log(cx * cx + cy * cy) / 2.;
		double nu      = log(log_zn / log(2.)) / log(2.);
		auto real_iter = iterated + 3.5 - nu;

		auto col1 = cuda_colormap[params.color_idx][get_color_idx(real_iter)];
		auto col2 = cuda_colormap[params.color_idx][get_color_idx(real_iter + 1.)];

		pixel = lerp_color(col1, col2, fmod(real_iter, 1.));
	}

	info.rendered = true;
}

MandelbrotCUDA::MandelbrotCUDA(SDL_Renderer* renderer) 
	: Mandelbrot(renderer)
{
	size_t size = surface->w * surface->h;

	cudaHostRegister(surface->pixels, size * sizeof(uint32_t), cudaHostRegisterDefault);
	cudaHostRegister(render_info.pixels, size * sizeof(PixelInfo), cudaHostRegisterDefault);

	cudaMalloc((void**)&device_surface, size * sizeof(uint32_t));
	cudaMalloc((void**)&device_pixel_info, size * sizeof(PixelInfo));

	cudaMemcpyToSymbol(cuda_colormap, colormap, sizeof(colormap));

	cudaStreamCreate(&streams[0]);
	cudaStreamCreate(&streams[1]);

	block_size = 8;

	update();
}

MandelbrotCUDA::~MandelbrotCUDA()
{
	cudaHostUnregister(surface->pixels);
	cudaHostUnregister(render_info.pixels);
	cudaFree(device_surface);
	cudaFree(device_pixel_info);
	cudaStreamDestroy(streams[0]);
	cudaStreamDestroy(streams[1]);
}

void MandelbrotCUDA::draw()
{
	size_t size = surface->w * surface->h * sizeof(uint32_t);

	cudaMemcpyAsync(surface->pixels, device_surface, size, cudaMemcpyDeviceToHost, streams[0]);
	cudaStreamSynchronize(streams[0]);
	Mandelbrot::draw();
}

void MandelbrotCUDA::stop()
{
	if (is_rendering) {
		size_t offset = offsetof(Constants, stop_all);

		cudaMemcpyToSymbolAsync(params, &is_rendering, 1, offset, cudaMemcpyHostToDevice, streams[0]);
		wait();
		cudaMemcpyToSymbolAsync(params, &is_rendering, 1, offset, cudaMemcpyHostToDevice, streams[0]);
	}
}

void MandelbrotCUDA::wait()
{
	cudaStreamSynchronize(streams[1]);
}

void MandelbrotCUDA::resize()
{
	int width, height;
	SDL_GetWindowSize(window, &width, &height);
	
	size_t size = width * height;

	stop();
	cudaFree(device_surface);
	cudaFree(device_pixel_info);

	cudaMalloc((void**)&device_surface, size * sizeof(uint32_t));
	cudaMalloc((void**)&device_pixel_info, size * sizeof(PixelInfo));
	
	cudaHostUnregister(surface->pixels);
	cudaHostUnregister(render_info.pixels);

	Mandelbrot::resize();

	cudaHostRegister(surface->pixels, size * sizeof(uint32_t), cudaHostRegisterDefault);
	cudaHostRegister(render_info.pixels, size * sizeof(PixelInfo), cudaHostRegisterDefault);
}

void MandelbrotCUDA::move(int32_t rel_px, int32_t rel_py)
{
	stop();

	size_t size = surface->w * surface->h;

	cudaMemcpy(surface->pixels, device_surface, size * sizeof(uint32_t), cudaMemcpyDeviceToHost);
	cudaMemcpy(render_info.pixels, device_pixel_info, size * sizeof(PixelInfo), cudaMemcpyDeviceToHost);

	Mandelbrot::move(rel_px, rel_py);
	
	cudaMemcpy(device_surface, surface->pixels, size * sizeof(uint32_t), cudaMemcpyHostToDevice);
	cudaMemcpy(device_pixel_info, render_info.pixels, size * sizeof(PixelInfo), cudaMemcpyHostToDevice);
}

void MandelbrotCUDA::drawSurface()
{
	dim3 grid((width - 1) / block_size + 1, (height - 1) / block_size + 1);
	dim3 block(block_size, block_size);

	real_t min_x = pos_x - 2. * scale * aspect;
	real_t max_y = pos_y + 2. * scale;
	real_t dp    = 4. * scale / height;

	mandelbrot_kernel<<<grid, block, 0, streams[1]>>>(device_surface, device_pixel_info, min_x, max_y, dp);
	cudaStreamSynchronize(streams[1]);
}

void MandelbrotCUDA::update(bool rerender_all, bool clear_surface)
{
	Mandelbrot::update(rerender_all, clear_surface);
	
	size_t size = surface->w * surface->h;

	if (rerender_all) 
		cudaMemset(device_pixel_info, 0, size * sizeof(PixelInfo));
	if (clear_surface)
		cudaMemset(device_surface, 0, size * sizeof(uint32_t));

	Constants constants = {
		width, height, iter, color_idx,color_scale, smooth, stop_all
	};

	cudaMemcpyToSymbol(params, &constants, sizeof(Constants));
}
CUDA C/C++

main.cpp에다가 mandelbrot.h를 포함시켜 주고 MandelbrotCUDA로 먼저 시작하도록 임시로 바꿔 줍니다.

main.cpp
#include "mandelbrot_tbb.h"
#include "mandelbrot_cuda.h"
...
	unique_ptr<Mandelbrot> mandelbrot = make_unique<MandelbrotCUDA>(renderer);

7-1-3. MandelbrotCUDA UI추가

위에서 main.cpp에 임시로 바꿔놓은 116번 줄을 원래대로 다시 돌려주고, gui.h와 gui.cpp를 수정합니다.

gui.h
	enum class Acc {
		CPU      = 0,
		CPU_TBB  = 1,
		GPU_CUDA = 2
	};
gui.cpp
#include "mandelbrot_tbb.h"
#include "mandelbrot_cuda.h"
#include "time.h"
...
	switch (settings.accelerator) {
	case Acc::CPU:
		mandelbrot = make_unique<Mandelbrot>(renderer);
		break;
	case Acc::CPU_TBB:
		mandelbrot = make_unique<MandelbrotTBB>(renderer);
		break;
	case Acc::GPU_CUDA:
		mandelbrot = make_unique<MandelbrotCUDA>(renderer);
		break;
	}
...
void GUI::showAcceleratorUI(std::unique_ptr<Mandelbrot>& mandelbrot)
{
	if (ImGui::CollapsingHeader("accelerator")) {
		static const char* items[] = { "CPU", "CPU - TBB", "GPU - CUDA" };
		
		ImGui::Text("accelerator :");
		if (ImGui::Combo(IMGUI_NO_LABEL, (int*)&settings.accelerator, items, 3))
			acceleratorChanged(mandelbrot);

		if (settings.accelerator == Acc::CPU_TBB) {
			auto* man_tbb = dynamic_cast<MandelbrotTBB*>(mandelbrot.get()); // will not fail

			auto value = (int)man_tbb->getMaxConcurrency();
			ImGui::Text("max concurrency:");
			if (ImGui::InputInt(IMGUI_NO_LABEL, &value, 1)) {
				value = SDL_clamp(value, 1, thread::hardware_concurrency());
				man_tbb->setMaxConcurrency(value);
			}
		} else if (settings.accelerator == Acc::GPU_CUDA) {
			static const char* sizes[] = { "1x1", "2x2", "4x4", "8x8", "16x16" };

			auto* man_cuda = dynamic_cast<MandelbrotCUDA*>(mandelbrot.get()); // will not fail

			auto block_size = (int)log2(man_cuda->getBlockSize());
			ImGui::Text("block size:");
			if (ImGui::Combo(IMGUI_NO_LABEL, &block_size, sizes, 5))
				man_cuda->setBlockSize(pow(block_size + 1, 2));
		}
	}
}

7-2. 멀티 샘플링

바로 아래 두 이미지는 지금까지의 과정으로 얻어낸 이미지입니다. 겉으로 봤을땐 나름 깔끔해 보이지만, 확대해보면 복잡한 부분의 디테일이 확연히 떨어집니다. 이제껏 구현된 방법은 정사각형 픽셀의 중앙에서 하나의 값을 샘플링하여 픽셀데이터를 만드는 것이었습니다. 여기서 멀티샘플링 기능은 하나의 값이 아니라 픽셀 범위에서 여러 값을 계산하여 하나의 픽셀에 적용하는 겁니다. 샘플을 추출하는 위치, 추출된 샘플을 하나의 픽셀로 처리하는 방법에 따라 여러 샘플링 기법이 나올 수 있습니다. 아래의 세 이미지는 각각 단일 샘플링, 균일 샘플링, 랜덤 샘플링을 보여줍니다. 여기서는 랜덤 샘플링을 구현해 보도록 하겠습니다.

7-2-2. 멀티 샘플링 기능 추가

멀티 샘플링은 샘플링의 횟수에 따라 그 계산량이 배로 증가합니다. 따라서 MandelbrotCUDA만 멀티샘플링 기능을 추가하도록 하겠습니다. 이 기능을 추가하려면 수정해야 할 부분이 매우 많으니 햇갈리지 않게 조심하도록 합니다.

먼저 mandelbrot_cuda.h에 샘플링 기능을 위한 멤버변수와 getter/setter를 추가합니다.

mandelbrot_cuda.h
	inline uint32_t getBlockSize() const { return block_size; }
	inline void setBlockSize(uint32_t size) { block_size = size; }

	inline uint32_t getTotalSample() const { return sample_total; }
	void setTotalSample(uint32_t sample);

	inline uint32_t getSamplePerLaunch() const { return sample_per_launch; }
	inline void setSamplePerLaunch(uint32_t sample) { sample_per_launch = sample; }

	inline uint32_t getSampleCount() const { return sample_count; }
...
	uint32_t block_size;
	uint32_t sample_total;
	uint32_t sample_per_launch;
	uint32_t sample_count;

멀티 샘플링 기능이 추가되며 각 픽셀에 지금껏 계산한 rgb값의 총량과 샘플링 횟수를 기록해야 합니다. 따라서 mandelbrot.h의 PixelInfo구조체를 수정합니다. 메모리를 좀 더 아끼기 위해 내부적으로 union으로 선언합니다. 이 수정으론 다행히 다른 클래스와 충돌을 일으키지 않습니다.

mandelbrot.h
	struct PixelInfo {
		union {
			struct {
				uint32_t sample_count;
				uint32_t acc_r;
				uint32_t acc_g;
				uint32_t acc_b;
			};
			bool rendered = false;
		};
	};

그리고 mandelbrot_cuda.cu를 수정합니다. 수정할 부분이 많으니 나눠서 설명하겠습니다.

make_color를 __device__로 선언해주고, 색상을 누적하는 acc_color() 함수를 추가합니다. 또 의사 난수를 생성하기 위한 선형 합동 생성기인 lcg()와 이를 이용해 0~1의 랜덤 float값을 출력하는 rnd()함수를 추가합니다. 샘플링을 위한 난수 생성은 엄청나게 좋은 품질의 난수를 요구하진 않습니다. 생성 속도가 더 우선시 되기 때문에 아주 간단한 알고리즘인 선형 합동 생성기를 사용합니다. 난수의 시드값으로 clock()을 사용합니다. GPU의 클록을 실시간으로 카운팅하는 변수인데 매번 시드값을 다르게 가지기 위해서 사용합니다.

mandelbrot_cuda.cu
template <class T>
__device__ inline uint32_t mandelbrot(T& cx, T& cy, uint32_t max_iter);
__device__ inline uint32_t make_color(uint8_t r, uint8_t g, uint8_t b, uint8_t a);
__device__ inline uint32_t lerp_color(uint32_t a, uint32_t b, double t);

__device__ inline void acc_color(uint32_t col, uint32_t& r, uint32_t& g, uint32_t& b) {
	struct Color { uint8_t b, g, r, a; } color = *(Color*)&col;

	r += color.r;
	g += color.g;
	b += color.b;
}

static __device__ __forceinline__ uint32_t lcg(uint32_t& prev)
{
	prev = (1664525u * prev + 1013904223u);
	return prev & 0x00FFFFFF;
}

static __device__ __forceinline__ float rnd(uint32_t& prev)
{
	return ((float)lcg(prev) / (float)0x01000000);
}
...
template <class T>
__device__ uint32_t get_color_idx(T iter) {
	return (uint32_t)(params.color_scale * 256 * iter / params.iter) % 256;
}

너무 많이 수정되서 강조 표시는 따로 안했습니다. 아래 코드를 보면 총 필요한 샘플링 수에 맞춰서 픽셀 데이터를 참조하여 계산하는 것을 알 수 있습니다. 예를 들어 50번 샘플링을 하는데 50번의 계산이 모두 끝난 후 화면에 표시한다면 뚝뚝 끊겨서 보이는 느낌이 심하기 때문에 최대 sample_per_launch라는 멤버변수에 설정된 값 만큼만 한번에 계산합니다. 누적된 rgb데이터를 하나의 픽셀데이터로 변환하는 방법은 단순히 평균을 구하는 것으로 처리합니다. 샘플링된 픽셀의 위치에 따라 다른 가중치를 주거나 심지어 인근 다른 픽셀에 까지 적용시키는 방법이 있지만 이 방법이 가장 간단하면서 효과적입니다.

__global__ void mandelbrot_kernel(
	uint32_t* device_surface,
	PixelInfo* device_pixel_info,
	uint32_t   sample_total,
	uint32_t   sample,
	real_t     min_x,
	real_t     max_y,
	real_t     dp
) {
	if (params.stop_all) return;

	int w = blockDim.x * blockIdx.x + threadIdx.x;
	int h = blockDim.x * blockIdx.y + threadIdx.y;

	if (params.width <= w || params.height <= h) return;

	auto pixel_off  = h * params.width + w;
	auto& info      = *(device_pixel_info + pixel_off);

	if (info.sample_count >= sample_total) return;
 
	auto seed  = (uint32_t)clock();
	auto count = min(sample_total - info.sample_count, sample);

	uint32_t col;
	uint32_t r = info.acc_r;
	uint32_t g = info.acc_g;
	uint32_t b = info.acc_b;

	for (uint32_t i = 0; i < count; ++i) {
		if (params.stop_all) return;
		
		real_t cx = min_x + dp * (w + rnd(seed));
		real_t cy = max_y - dp * (h + rnd(seed));

		auto iterated = mandelbrot<real_t>(cx, cy, params.iter);

		if (iterated == params.iter) {
			col = 0xff000000;
		} else if (!params.color_smooth) {
			col = cuda_colormap[params.color_idx][get_color_idx(iterated)];
		} else {
			double log_zn  = log(cx * cx + cy * cy) / 2.;
			double nu      = log(log_zn / log(2.)) / log(2.);
			auto real_iter = iterated + 3.5 - nu;

			auto col1 = cuda_colormap[params.color_idx][get_color_idx(real_iter)];
			auto col2 = cuda_colormap[params.color_idx][get_color_idx(real_iter + 1.)];

			col = lerp_color(col1, col2, fmod(real_iter, 1.));
		}

		acc_color(col, r, g, b);
	}

	info.sample_count += count;
	info.acc_r         = r;
	info.acc_g         = g;
	info.acc_b         = b;

	auto& pixel = *(device_surface + pixel_off);
	pixel = make_color(r / info.sample_count, g / info.sample_count, b / info.sample_count);
}

이제 나머지 부분을 수정합니다. 246번 줄 부터 sample_per_launch에 맞춰 여러 번 샘플링을 하는 부분이 있습니다.

mandelbrot_cuda.cu
	block_size        = 8;
	sample_total      = 1;
	sample_per_launch = 1;
	sample_count      = 0;
...
void MandelbrotCUDA::setTotalSample(uint32_t sample)
{
	auto clear = sample_count > (sample_total = sample);
	update(clear, clear);
}
...
void MandelbrotCUDA::drawSurface()
{
	dim3 grid((width - 1) / block_size + 1, (height - 1) / block_size + 1);
	dim3 block(block_size, block_size);

	real_t min_x = pos_x - 2. * scale * aspect;
	real_t max_y = pos_y + 2. * scale;
	real_t dp    = 4. * scale / height;

	while (sample_count < sample_total) {
		if (stop_all) break;

		mandelbrot_kernel<<<grid, block, 0, streams[1]>>> (
			device_surface,
			device_pixel_info,
			sample_total,
			sample_per_launch,
			min_x, max_y, dp);

		sample_count = min(sample_count + sample_per_launch, sample_total);
		cudaStreamSynchronize(streams[1]);
	}

	cudaStreamSynchronize(streams[1]);
}

void MandelbrotCUDA::update(bool rerender_all, bool clear_surface)
{
	Mandelbrot::update(rerender_all, clear_surface);
	
	size_t size = surface->w * surface->h;

	if (rerender_all) {
		cudaMemset(device_pixel_info, 0, size * sizeof(PixelInfo));
		sample_count = 0;
	}
	if (clear_surface)
		cudaMemset(device_surface, 0, size * sizeof(uint32_t));

	Constants constants = {
		width, height, iter, color_idx,color_scale, smooth, stop_all
	};

	cudaMemcpyToSymbol(params, &constants, sizeof(Constants));
}

마지막으로 UI를 추가해 줍니다.

gui.cpp
			static const char* sizes[] = { "1x1", "2x2", "4x4", "8x8", "16x16" };

			auto* man_cuda = dynamic_cast<MandelbrotCUDA*>(mandelbrot.get()); // will not fail

			auto block_size = (int)log2(man_cuda->getBlockSize());
			ImGui::Text("block size:");
			if (ImGui::Combo(IMGUI_NO_LABEL, &block_size, sizes, 5))
				man_cuda->setBlockSize(pow(block_size + 1, 2));

			auto sample_total = (int)man_cuda->getTotalSample();
			ImGui::Text("total sample:");
			if (ImGui::InputInt(IMGUI_NO_LABEL, &sample_total))
				man_cuda->setTotalSample(sample_total = max(sample_total, 1));
			
			auto spl = (int)man_cuda->getSamplePerLaunch();
			ImGui::Text("sample per launch:");
			ImGui::InputInt(IMGUI_NO_LABEL, &spl);
			man_cuda->setSamplePerLaunch(spl = SDL_clamp(spl, 1, sample_total));

			ImGui::Text("sampled: %d", man_cuda->getSampleCount());

왼쪽이 단일 샘플링 오른쪽이 25번 샘플링한 결과입니다. 확실히 곡선과 픽셀의 자글자글한 부분이 부드럽게 변한 모습을 볼 수 있습니다.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다