diff --git a/src/drawdance/libcommon/dpcommon/common.h b/src/drawdance/libcommon/dpcommon/common.h index a7c957b36..0e8f9e8bf 100644 --- a/src/drawdance/libcommon/dpcommon/common.h +++ b/src/drawdance/libcommon/dpcommon/common.h @@ -268,9 +268,11 @@ DP_INLINE size_t DP_flex_size(size_t type_size, size_t flex_offset, * member. Takes potential trailing padding being used as part of the flexible * array member into account. */ +// NOLINTBEGIN(bugprone-sizeof-expression) #define DP_FLEX_SIZEOF(TYPE, FIELD, COUNT) \ DP_flex_size(sizeof(TYPE), offsetof(TYPE, FIELD), \ sizeof(((TYPE *)NULL)->FIELD[0]), COUNT) +// NOLINTEND(bugprone-sizeof-expression) void *DP_malloc(size_t size) DP_MALLOC_ATTR; diff --git a/src/drawdance/libengine/dpengine/renderer.c b/src/drawdance/libengine/dpengine/renderer.c new file mode 100644 index 000000000..7956c1631 --- /dev/null +++ b/src/drawdance/libengine/dpengine/renderer.c @@ -0,0 +1,378 @@ +#include "renderer.h" +#include "canvas_diff.h" +#include "canvas_state.h" +#include "layer_content.h" +#include "pixels.h" +#include "tile.h" +#include "view_mode.h" +#include +#include +#include +#include +#include + +#define TILE_QUEUE_INITIAL_CAPACITY 1024 + +typedef struct DP_RenderContext { + DP_ALIGNAS_SIMD DP_Pixel8 pixels[DP_TILE_LENGTH]; + DP_ViewModeBuffer *vmb; +} DP_RenderContext; + +typedef enum DP_RenderJobType { + DP_RENDER_JOB_TILE, + DP_RENDER_JOB_RESIZE, + DP_RENDER_JOB_WAIT, + DP_RENDER_JOB_INVALID, + DP_RENDER_JOB_QUIT, +} DP_RenderJobType; + +typedef struct DP_RendererTileCoords { + int tile_x, tile_y; +} DP_RendererTileCoords; + +typedef struct DP_RendererTileJob { + int tile_x, tile_y; + DP_CanvasState *cs; +} DP_RendererTileJob; + +typedef struct DP_RendererResizeJob { + int offset_x, offset_y; + int prev_width, prev_height; + DP_PaintEngineResizedFn resized; + void *user; +} DP_RendererResizeJob; + +typedef struct DP_RenderJob { + DP_RenderJobType type; + union { + int dummy; // Shut up overeager compiler warnings about initialization. + DP_RendererTileJob tile; + DP_RendererResizeJob resize; + }; +} DP_RenderJob; + +struct DP_Renderer { + DP_RenderContext *contexts; + DP_Queue resize_queue; + DP_Queue tile_queue; + DP_CanvasState *cs; + DP_Mutex *queue_mutex; + DP_Semaphore *queue_sem; + DP_Semaphore *wait_ready_sem; + DP_Semaphore *wait_done_sem; + int thread_count; + DP_Thread *threads[]; +}; + + +static DP_TransientTile *flatten_tile(DP_CanvasState *cs, + DP_ViewModeBuffer *vmb, + bool needs_checkers, int tile_index) +{ + DP_TransientTile *tt = DP_transient_tile_new_nullable( + DP_canvas_state_background_tile_noinc(cs), 0); + DP_ViewModeFilter vmf = + DP_local_state_view_mode_filter_make(pe->local_state, vmb, cs); + DP_canvas_state_flatten_tile_to(cs, tile_index, tt, true, &vmf); + + if (needs_checkers) { + DP_transient_tile_merge(tt, pe->checker, DP_BIT15, + DP_BLEND_MODE_BEHIND); + } + DP_transient_layer_content_transient_tile_set_noinc(pe->tlc, tt, + tile_index); + return tt; +} + +static void render_job(void *user, int thread_index) +{ + struct DP_PaintEngineRenderJobParams *job_params = user; + struct DP_PaintEngineRenderParams *render_params = + job_params->render_params; + int x = job_params->x; + int y = job_params->y; + + DP_PaintEngine *pe = render_params->pe; + int tile_index = y * render_params->xtiles + x; + DP_TransientTile *tt = + flatten_tile(pe, pe->render.buffers[thread_index].vmb, + render_params->needs_checkers, tile_index); + + DP_Pixel8 *pixel_buffer = pe->render.buffers[thread_index].pixels; + DP_pixels15_to_8_tile(pixel_buffer, DP_transient_tile_pixels(tt)); + + render_params->render_tile(render_params->user, x, y, pixel_buffer, + thread_index); + + DP_SEMAPHORE_MUST_POST(pe->render.tiles_done_sem); +} + +static void handle_tile_job(DP_Renderer *renderer, DP_RenderContext *rc, + DP_RendererTileJob *job) +{ + (void)renderer; + (void)rc; + (void)job; + + DP_TransientTile *tt = + flatten_tile(pe, rc->vmb, render_params->needs_checkers, tile_index); +} + + +static void handle_resize_job(DP_Renderer *renderer, DP_RendererResizeJob *job) +{ + // Wait for all threads to be in a safe state before proceeding. + int wait_thread_count = renderer->thread_count - 1; + DP_SEMAPHORE_MUST_WAIT_N(renderer->wait_ready_sem, wait_thread_count); + + job->resized(job->user, job->offset_x, job->offset_y, job->prev_width, + job->prev_height); + + // Unlock the other threads. + DP_SEMAPHORE_MUST_POST_N(renderer->wait_done_sem, wait_thread_count); +} + + +static void handle_wait_job(DP_Renderer *renderer) +{ + DP_SEMAPHORE_MUST_POST(renderer->wait_ready_sem); + DP_SEMAPHORE_MUST_WAIT(renderer->wait_done_sem); +} + + +static DP_RenderJob dequeue_job(DP_Renderer *renderer) +{ + DP_Queue *resize_queue = &renderer->resize_queue; + DP_RenderJob *resize_job = DP_queue_peek(resize_queue, sizeof(*resize_job)); + if (resize_job) { + DP_RenderJob job = *resize_job; + DP_queue_shift(resize_queue); + return job; + } + + DP_Queue *tile_queue = &renderer->tile_queue; + DP_RendererTileCoords *coords = DP_queue_peek(tile_queue, sizeof(*coords)); + if (coords) { + DP_RenderJob job; + if (coords->tile_x >= 0) { + job.type = DP_RENDER_JOB_TILE; + job.tile = (DP_RendererTileJob){ + coords->tile_x, + coords->tile_y, + DP_canvas_state_incref(renderer->cs), + }; + } + else { + job.type = DP_RENDER_JOB_INVALID; + } + DP_queue_shift(tile_queue); + return job; + } + + return (DP_RenderJob){DP_RENDER_JOB_QUIT, {0}}; +} + +static void handle_jobs(DP_Renderer *renderer, int thread_index) +{ + DP_Mutex *queue_mutex = renderer->queue_mutex; + DP_Semaphore *queue_sem = renderer->queue_sem; + DP_RenderContext *rc = &renderer->contexts[thread_index]; + while (true) { + DP_SEMAPHORE_MUST_WAIT(queue_sem); + DP_MUTEX_MUST_LOCK(queue_mutex); + DP_RenderJob job = dequeue_job(renderer); + DP_MUTEX_MUST_UNLOCK(queue_mutex); + switch (job.type) { + case DP_RENDER_JOB_TILE: + handle_tile_job(renderer, rc, &job.tile); + break; + case DP_RENDER_JOB_RESIZE: + handle_resize_job(renderer, &job.resize); + break; + case DP_RENDER_JOB_WAIT: + handle_wait_job(renderer); + break; + case DP_RENDER_JOB_INVALID: + break; + case DP_RENDER_JOB_QUIT: + return; + default: + DP_UNREACHABLE(); + } + } +} + +struct DP_RenderWorkerParams { + DP_Renderer *renderer; + int thread_index; +}; + +static void run_worker_thread(void *user) +{ + struct DP_RenderWorkerParams *params = user; + DP_Renderer *renderer = params->renderer; + int thread_index = params->thread_index; + DP_free(params); + handle_jobs(renderer, thread_index); +} + + +DP_Renderer *DP_renderer_new(int thread_count) +{ + DP_ASSERT(thread_count > 0); + size_t size_thread_count = DP_int_to_size(thread_count); + DP_Renderer *renderer = + DP_malloc(DP_FLEX_SIZEOF(DP_Renderer, threads, size_thread_count)); + renderer->thread_count = thread_count; + renderer->queue_mutex = NULL; + renderer->queue_sem = NULL; + renderer->wait_ready_sem = NULL; + renderer->wait_done_sem = NULL; + DP_queue_init(&renderer->resize_queue, size_thread_count * 2, + sizeof(DP_RenderJob)); + DP_queue_init(&renderer->tile_queue, TILE_QUEUE_INITIAL_CAPACITY, + sizeof(DP_RendererTileCoords)); + renderer->cs = DP_canvas_state_new(); + renderer->contexts = DP_malloc_simd(sizeof(*renderer->contexts) + * DP_int_to_size(thread_count)); + for (int i = 0; i < thread_count; ++i) { + renderer->contexts[i].vmb = DP_view_mode_buffer_new(); + renderer->threads[i] = NULL; + } + +#define RENDERER_ENSURE(X) \ + do { \ + if (!(X)) { \ + DP_renderer_free(renderer); \ + return NULL; \ + } \ + } while (0) + + RENDERER_ENSURE(renderer->queue_mutex = DP_mutex_new()); + RENDERER_ENSURE(renderer->queue_sem = DP_semaphore_new(0)); + RENDERER_ENSURE(renderer->wait_ready_sem = DP_semaphore_new(0)); + RENDERER_ENSURE(renderer->wait_done_sem = DP_semaphore_new(0)); + +#undef RENDERER_ENSURE + + for (int i = 0; i < thread_count; ++i) { + struct DP_RenderWorkerParams *params = DP_malloc(sizeof(*params)); + *params = (struct DP_RenderWorkerParams){renderer, i}; + renderer->threads[i] = DP_thread_new(run_worker_thread, params); + if (!renderer->threads[i]) { + DP_free(params); + DP_renderer_free(renderer); + return NULL; + } + } + + return renderer; +} + +void DP_renderer_free(DP_Renderer *renderer) +{ + if (renderer) { + int thread_count = renderer->thread_count; + DP_MUTEX_MUST_LOCK(renderer->queue_mutex); + renderer->resize_queue.used = 0; + renderer->tile_queue.used = 0; + DP_SEMAPHORE_MUST_POST_N(renderer->queue_sem, thread_count); + DP_MUTEX_MUST_UNLOCK(renderer->queue_mutex); + + for (int i = 0; i < thread_count; ++i) { + DP_thread_free_join(renderer->threads[i]); + } + DP_semaphore_free(renderer->wait_done_sem); + DP_semaphore_free(renderer->wait_ready_sem); + DP_semaphore_free(renderer->queue_sem); + DP_mutex_free(renderer->queue_mutex); + DP_canvas_state_decref(renderer->cs); + DP_queue_dispose(&renderer->tile_queue); + DP_queue_dispose(&renderer->resize_queue); + for (int i = 0; i < thread_count; ++i) { + DP_view_mode_buffer_free(renderer->contexts[i].vmb); + } + DP_free_simd(renderer->contexts); + DP_free(renderer); + } +} + + +static void invalidate_tile_coords(void *element, DP_UNUSED void *user) +{ + DP_RendererTileCoords *coords = element; + coords->tile_x = -1; +} + +void DP_renderer_resize(DP_Renderer *renderer, int offset_x, int offset_y, + int prev_width, int prev_height, + DP_PaintEngineResizedFn resized, void *user) +{ + DP_ASSERT(renderer); + DP_ASSERT(resized); + + DP_Mutex *queue_mutex = renderer->queue_mutex; + DP_Queue *resize_queue = &renderer->resize_queue; + DP_Queue *tile_queue = &renderer->tile_queue; + int thread_count = renderer->thread_count; + int wait_thread_count = renderer->thread_count - 1; + DP_MUTEX_MUST_LOCK(queue_mutex); + + // Resizing must be synchronized between all render threads, since all tile + // positions are invalidated by the operation. So we enqueue a wait job for + // all except one thread, plus the actual resize job for the last one. + for (int i = 0; i < wait_thread_count; ++i) { + DP_RenderJob *wait_job = DP_queue_push(resize_queue, sizeof(*wait_job)); + wait_job->type = DP_RENDER_JOB_WAIT; + } + + DP_RenderJob *resize_job = DP_queue_push(resize_queue, sizeof(*resize_job)); + resize_job->type = DP_RENDER_JOB_RESIZE; + resize_job->resize = (DP_RendererResizeJob){ + offset_x, offset_y, prev_width, prev_height, resized, user}; + + DP_SEMAPHORE_MUST_POST_N(renderer->queue_sem, thread_count); + + // All current tile jobs are invalidated by the resize, so we turn those + // into tombstones to avoid any pointless processing thereof. + DP_queue_each(tile_queue, sizeof(DP_RendererTileCoords), + invalidate_tile_coords, NULL); + + DP_MUTEX_MUST_UNLOCK(queue_mutex); +} + + +struct DP_RendererPushTileParams { + DP_Queue *tile_queue; + int pushed; +}; + +static void push_tile(void *user, int tile_x, int tile_y) +{ + struct DP_RendererPushTileParams *params = user; + // TODO: deduplicate to avoid re-rendering the same tile. + DP_RendererTileCoords *coords = + DP_queue_push(params->tile_queue, sizeof(*coords)); + *coords = (DP_RendererTileCoords){tile_x, tile_y}; + ++params->pushed; +} + +void DP_renderer_apply(DP_Renderer *renderer, DP_CanvasState *cs, + DP_CanvasDiff *diff) +{ + DP_ASSERT(renderer); + DP_ASSERT(cs); + DP_ASSERT(diff); + + DP_Mutex *queue_mutex = renderer->queue_mutex; + struct DP_RendererPushTileParams params = {&renderer->tile_queue, 0}; + DP_MUTEX_MUST_LOCK(queue_mutex); + + DP_canvas_state_decref(renderer->cs); + renderer->cs = DP_canvas_state_incref(cs); + + DP_canvas_diff_each_pos_reset(diff, push_tile, ¶ms); + DP_SEMAPHORE_MUST_POST_N(renderer->queue_sem, params.pushed); + + DP_MUTEX_MUST_UNLOCK(queue_mutex); +} diff --git a/src/drawdance/libengine/dpengine/renderer.h b/src/drawdance/libengine/dpengine/renderer.h new file mode 100644 index 000000000..6402e2302 --- /dev/null +++ b/src/drawdance/libengine/dpengine/renderer.h @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +#include "paint_engine.h" +#include + +typedef struct DP_CanvasDiff DP_CanvasDiff; +typedef struct DP_CanvasState DP_CanvasState; +typedef struct DP_Renderer DP_Renderer; + + +DP_Renderer *DP_renderer_new(int thread_count); + +void DP_renderer_free(DP_Renderer *renderer); + +void DP_renderer_resize(DP_Renderer *renderer, int offset_x, int offset_y, + int prev_width, int prev_height, + DP_PaintEngineResizedFn resized, void *user); + +// Increments refcount on the given canvas state, resets the given diff. +void DP_renderer_apply(DP_Renderer *renderer, DP_CanvasState *cs, + DP_CanvasDiff *diff);