#include "CanvasLayer.h" #include #include "../GPU.h" #include "../Assets.h" #include "../Input/InputHandler.h" #include "../App.h" #include "../AppLayers/NotifyLayer.h" #include "../ManagementContext/LayerManagmentContext.h" #include "../PrimitivesBuilder/PipelineBuilder.h" #include "../PrimitivesBuilder/VertexInputStateBuilder.h" #include "../UserInterface/Popup.h" void CanvasAppLayer::uploadBuffers() { SDL_GPUBufferCreateInfo buf_info = { .usage = SDL_GPU_BUFFERUSAGE_VERTEX, .size = sizeof(previewVertices), .props = {} }; previewBuffer = new GPUBuffer(&buf_info); canvasBuffer = new GPUBuffer(&buf_info); if (!previewBuffer) { throw std::runtime_error("Failed to create vertex buffer"); } SDL_GPUSamplerCreateInfo samplerInfo = {}; samplerInfo.min_filter = SDL_GPU_FILTER_LINEAR; samplerInfo.mag_filter = SDL_GPU_FILTER_LINEAR; samplerInfo.mipmap_mode = SDL_GPU_SAMPLERMIPMAPMODE_LINEAR; samplerInfo.address_mode_u = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; samplerInfo.address_mode_v = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; samplerInfo.address_mode_w = SDL_GPU_SAMPLERADDRESSMODE_CLAMP_TO_EDGE; samplerInfo.mip_lod_bias = 0.0f; samplerInfo.max_anisotropy = 1.0f; samplerInfo.compare_op = SDL_GPU_COMPAREOP_NEVER; samplerInfo.min_lod = 0.0f; samplerInfo.max_lod = 1000.0f; sampler = SDL_CreateGPUSampler(GPU::GetDevice(), &samplerInfo); if (!sampler) { throw std::runtime_error(SDL_GetError()); } } void CanvasAppLayer::setVertexBuffer(SDL_FRect fullRect, SDL_FRect previewRect) { #define NDC_TO_SCREEN * 2.0f - 1.0f // parameters are using screen space // bottom left previewVertices[0] = (previewRect.x - fullRect.x) / fullRect.w NDC_TO_SCREEN; previewVertices[1] = (previewRect.y - fullRect.y) / fullRect.h NDC_TO_SCREEN; previewVertices[2] = (previewRect.x - fullRect.x) / fullRect.w; // match 0 previewVertices[3] = (previewRect.y - fullRect.y) / fullRect.h; // match 13 // bottom right previewVertices[4] = (previewRect.x + previewRect.w - fullRect.x) / fullRect.w NDC_TO_SCREEN; previewVertices[5] = (previewRect.y - fullRect.y) / fullRect.h NDC_TO_SCREEN; previewVertices[6] = (previewRect.x + previewRect.w - fullRect.x) / fullRect.w; // match 4 previewVertices[7] = (previewRect.y - fullRect.y) / fullRect.h; // match 13 // top left previewVertices[8] = (previewRect.x - fullRect.x) / fullRect.w NDC_TO_SCREEN; previewVertices[9] = (previewRect.y + previewRect.h - fullRect.y) / fullRect.h NDC_TO_SCREEN; previewVertices[10] = (previewRect.x - fullRect.x) / fullRect.w; // match 8 previewVertices[11] = (previewRect.y + previewRect.h - fullRect.y) / fullRect.h; // match 1 // top right previewVertices[12] = (previewRect.x + previewRect.w - fullRect.x) / fullRect.w NDC_TO_SCREEN; previewVertices[13] = (previewRect.y + previewRect.h - fullRect.y) / fullRect.h NDC_TO_SCREEN; previewVertices[14] = (previewRect.x + previewRect.w - fullRect.x) / fullRect.w; // match 12 previewVertices[15] = (previewRect.y + previewRect.h - fullRect.y) / fullRect.h; // match 1 // invert y for (int i = 1; i < 16; i += 4) { previewVertices[i] = -1.0f * previewVertices[i]; } #undef NDC_TO_SCREEN previewBuffer->Transfer(previewVertices); } void CanvasAppLayer::setVertexBufferCanvas() { SDL_FPoint corners[4] = { { 0.0f, 0.0f }, { 1.0f, 0.0f }, { 0.0f, 1.0f }, { 1.0f, 1.0f } }; for (int i = 0; i < 4; i++) { computeVertex(corners[i].x, corners[i].y, previewVertices[i * 4], previewVertices[i * 4 + 1]); previewVertices[i * 4 + 2] = corners[i].x; // Restored your intentional UV inversion here: previewVertices[i * 4 + 3] = corners[i * -1 + 3].y; } canvasBuffer->Transfer(previewVertices); } void CanvasAppLayer::computeVertex(float cx, float cy, float& outX, float& outY) { // orgin float px = (cx - 0.5f) * LayerManagementContext::FinalWidth; float py = (cy - 0.5f) * LayerManagementContext::FinalHeight; // rotation float cosR = cosf(Rotation); float sinR = sinf(Rotation); float rx = px * cosR - py * sinR; float ry = px * sinR + py * cosR; // zoom rx *= Zoom; ry *= Zoom; // ndc pan outX = (rx / (App::Width * 0.5f)) + (PanX * 2 / App::Width); outY = (ry / (App::Height * 0.5f)) + (PanY * 2 / App::Height); } void CanvasAppLayer::createPipelines() { vertShader = new Shader(Assets::Path("shaders/CanvasRenderingTarget.vert.spirv"), "main", 0, 0, 0, 0); fragShader = new Shader(Assets::Path("shaders/CanvasRenderingTarget.frag.spirv"), "main", 1, 0, 0, 1); transparencyFragShader = new Shader(Assets::Path("shaders/TransparencyField.frag.spirv"), "main", 0, 0, 0, 1); auto vertexInputState = VertexInputStateBuilder() .AddAttribute(0, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2) .AddAttribute(1, SDL_GPU_VERTEXELEMENTFORMAT_FLOAT2) .BuildDeferred(); pipelineBlend = PipelineBuilder() .Shaders(vertShader, fragShader) .FormatSwapchain() .BlendPremultiplied() .Primitive(SDL_GPU_PRIMITIVETYPE_TRIANGLESTRIP) .Vertex(vertexInputState.Use()) .Build(); pipelineLocalPreview = PipelineBuilder() .Shaders(vertShader, fragShader) .Format(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM) .BlendAlpha() .Primitive(SDL_GPU_PRIMITIVETYPE_TRIANGLESTRIP) .Vertex(vertexInputState.Use()) .Build(); pipelineLocalPreviewEraser = PipelineBuilder() .Shaders(vertShader, fragShader) .Format(SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM) .BlendErase() .Primitive(SDL_GPU_PRIMITIVETYPE_TRIANGLESTRIP) .Vertex(vertexInputState.Use()) .Build(); pipelineTransparencyField = PipelineBuilder() .Shaders(vertShader, transparencyFragShader) .FormatSwapchain() .BlendPremultiplied() .Primitive(SDL_GPU_PRIMITIVETYPE_TRIANGLESTRIP) .Vertex(vertexInputState.Use()) .Build(); } #include "../LayerNodes/PaintLayerNode.h" void CanvasAppLayer::renderPreviewStage(SDL_GPUColorTargetInfo swapchainTargetInfo, SDL_GPUViewport viewport) { auto* paintLayer = dynamic_cast(LayerManagementContext::SelectedLayer); if (!paintLayer) return; paintLayer->Target->CopyTo(localPreview, { 0, 0 }, { 0, 0 }, { LayerManagementContext::FinalWidth, LayerManagementContext::FinalHeight }); SDL_FRect previewBoundingBox = Tool::GetActive()->GetPreviewBoundingBox(); SDL_GPUViewport viewport2 = {}; viewport2.x = 0.0f; viewport2.y = 0.0f; viewport2.w = (float)LayerManagementContext::FinalWidth; viewport2.h = (float)LayerManagementContext::FinalHeight; viewport2.min_depth = 0.0f; viewport2.max_depth = 1.0f; SDL_FRect viewrect = *((SDL_FRect*)&viewport2); // we love a bit of memory tomfoolery :3 // the address of the viewport, as a pointer to an SDL_FRect, dereferenced. setVertexBuffer(viewrect, previewBoundingBox); // Blend ontop SDL_GPUColorTargetInfo previewTargetInfo = {}; previewTargetInfo.texture = localPreview->Get(); previewTargetInfo.clear_color = { 0.0f, 0.0f, 0.0f, 0.0f }; previewTargetInfo.load_op = SDL_GPU_LOADOP_LOAD; previewTargetInfo.store_op = SDL_GPU_STOREOP_STORE; previewTargetInfo.mip_level = 0; previewTargetInfo.layer_or_depth_plane = 0; previewTargetInfo.cycle = false; GPU::AquirePushCommandBuffer(); renderFinalCompositeToTarget({ &previewTargetInfo }, preview, viewport2, Tool::GetActive()->IsEraser() ? pipelineLocalPreviewEraser : pipelineLocalPreview, previewBuffer, *Tool::GetActive()->Opacity); GPU::SubmitCommandsPopCommandBuffer(); // Render replace layerRenderingContext->CompositeReplacedSelectedLayer(localPreview)->CopyTo(localPreview, { 0, 0 }, { 0, 0 }, { LayerManagementContext::FinalWidth, LayerManagementContext::FinalHeight }); setVertexBufferCanvas(); renderFinalCompositeToTarget({ &swapchainTargetInfo }, localPreview, viewport, pipelineBlend, canvasBuffer); } void CanvasAppLayer::setPanLimited() { // goal is, to keep at least one corner of the canvas witin a part of the screen area at all times accounting for rotation and zoom double safeAreaHeight = App::Height / 2.0f; double safeAreaWidth = App::Width / 2.0f; // seperate axis theorem // half-dimensions float Shx = safeAreaWidth / 2.0f; float Shy = safeAreaHeight / 2.0f; float Chx = (LayerManagementContext::FinalWidth * Zoom) / 2.0f; float Chy = (LayerManagementContext::FinalHeight * Zoom) / 2.0f; // rotation components float cosR = cosf(Rotation); float sinR = sinf(Rotation); float absCos = fabsf(cosR); float absSin = fabsf(sinR); // siffrin for (int i = 0; i < 3; i++) { // screen x float bound1 = Shx + Chx * absCos + Chy * absSin; if (PanX > bound1) PanX -= (PanX - bound1); else if (PanX < -bound1) PanX += (-bound1 - PanX); // screen y float bound2 = Shy + Chx * absSin + Chy * absCos; if (PanY > bound2) PanY -= (PanY - bound2); else if (PanY < -bound2) PanY += (-bound2 - PanY); // local x float p3 = PanX * cosR + PanY * sinR; float bound3 = Chx + Shx * absCos + Shy * absSin; if (p3 > bound3) { PanX -= (p3 - bound3) * cosR; PanY -= (p3 - bound3) * sinR; } else if (p3 < -bound3) { PanX += (-bound3 - p3) * cosR; PanY += (-bound3 - p3) * sinR; } // local y float p4 = -PanX * sinR + PanY * cosR; float bound4 = Chy + Shx * absSin + Shy * absCos; if (p4 > bound4) { PanX -= (p4 - bound4) * -sinR; PanY -= (p4 - bound4) * cosR; } else if (p4 < -bound4) { PanX += (-bound4 - p4) * -sinR; PanY += (-bound4 - p4) * cosR; } } } void CanvasAppLayer::renderToSwapchain() { SDL_GPUColorTargetInfo swapchainTargetInfo = {}; swapchainTargetInfo.texture = GPU::GetSwapchain(); swapchainTargetInfo.load_op = SDL_GPU_LOADOP_LOAD; swapchainTargetInfo.store_op = SDL_GPU_STOREOP_STORE; swapchainTargetInfo.mip_level = 0; swapchainTargetInfo.layer_or_depth_plane = 0; swapchainTargetInfo.cycle = false; int width, height; SDL_GetWindowSizeInPixels(GPU::GetWindow(), &width, &height); SDL_GPUViewport viewport = {}; viewport.x = 0.0f; viewport.y = 0.0f; viewport.w = (float)width; viewport.h = (float)height; //setVertexBuffer({ 0.0f, 0.0f, (float)width, (float)height }, { 0.0f, 0.0f, (float)renderingTarget->GetWidth(), (float)renderingTarget->GetHeight() }); setVertexBufferCanvas(); renderTransparencyField(&swapchainTargetInfo, viewport); if (preview) { renderPreviewStage(swapchainTargetInfo, viewport); } else { //SDL_FRect viewrect = *((SDL_FRect*)&viewport); //setVertexBuffer(viewrect, { 0.0f, 0.0f, (float)renderingTarget->GetWidth(), (float)renderingTarget->GetHeight() }); setVertexBufferCanvas(); renderFinalCompositeToTarget({ &swapchainTargetInfo }, layerRenderingContext->CompositeAll(LayerManagementContext::Layers), viewport, pipelineBlend, canvasBuffer); // ask LayerRenderingContext for composite → replace renderingTarget } } void CanvasAppLayer::renderFinalCompositeToTarget(std::list targets, Texture* source, SDL_GPUViewport viewport, GraphicsPipeline* pipeline, GPUBuffer* inputBuffer, float opacity) { GPU::BeginRendering(targets, nullptr); SDL_SetGPUViewport(GPU::GetRenderPass(), &viewport); pipeline->Bind(); SDL_GPUBufferBinding vertexBufferBinding = { .buffer = inputBuffer->Get(), .offset = 0 }; SDL_BindGPUVertexBuffers(GPU::GetRenderPass(), 0, &vertexBufferBinding, 1); SDL_GPUTextureSamplerBinding textureSamplerBinding = { .texture = source->Get(), .sampler = sampler }; SDL_BindGPUFragmentSamplers(GPU::GetRenderPass(), 0, &textureSamplerBinding, 1); float pushConstants[1] = { opacity }; SDL_PushGPUFragmentUniformData(GPU::GetCommandBuffer(), 0, pushConstants, sizeof(pushConstants)); SDL_DrawGPUPrimitives(GPU::GetRenderPass(), 4, 1, 0, 0); // draw canvas GPU::EndRendering(); } void CanvasAppLayer::renderTransparencyField(SDL_GPUColorTargetInfo* target, SDL_GPUViewport viewport) { float lightCol[4] = { 1.0f, 1.0f, 1.0f, 1.0f }; float darkCol[4] = { 0.75f, 0.75f, 0.75f, 1.0f }; float darkerCol[4] = { 0.5f, 0.5f, 0.5f, 1.0f }; switch (LayerManagementContext::Background) { case BackgroundMode::COLOR: memcpy(lightCol, LayerManagementContext::BackgroundColor, sizeof(float) * 4); memcpy(darkCol, LayerManagementContext::BackgroundColor, sizeof(float) * 4); break; case BackgroundMode::TRANSPARENT: break; // already set case BackgroundMode::TRANSPARENT_DARK: memcpy(lightCol, darkCol, sizeof(float) * 4); memcpy(darkCol, darkerCol, sizeof(float) * 4); break; } GPU::BeginRendering({ target }, nullptr); SDL_SetGPUViewport(GPU::GetRenderPass(), &viewport); pipelineTransparencyField->Bind(); SDL_GPUBufferBinding vertexBufferBinding = { .buffer = canvasBuffer->Get(), .offset = 0 }; SDL_BindGPUVertexBuffers(GPU::GetRenderPass(), 0, &vertexBufferBinding, 1); float pushConstants[13] = { // light col lightCol[0], lightCol[1], lightCol[2], lightCol[3], // dark col darkCol[0], darkCol[1], darkCol[2], darkCol[3], // frag size width and height (float)LayerManagementContext::FinalWidth, (float)LayerManagementContext::FinalHeight, // gridsize 16.0f }; SDL_PushGPUFragmentUniformData(GPU::GetCommandBuffer(), 0, pushConstants, sizeof(pushConstants)); SDL_DrawGPUPrimitives(GPU::GetRenderPass(), 4, 1, 0, 0); // draw canvas GPU::EndRendering(); } void CanvasAppLayer::OnActivate() { uploadBuffers(); createPipelines(); layerRenderingContext = new LayerRenderingContext(); CreateCanvas(LayerManagementContext::FinalWidth, LayerManagementContext::FinalHeight); isActive = true; } bool CanvasAppLayer::OnDown(std::shared_ptr stream) { if (!Tool::GetActive()) { return false; } if (PanelManagementContext::GetMode() != PanelManagementMode::DEFAULT) { return false; } if (Popup::GetCount() > 0) { return false; } PointerAction initialAction = (PointerAction)stream->GetInitialInput().action; if ((InputHandler::IsKeyHeld(SDLK_LSHIFT) && InputHandler::IsKeyHeld(SDLK_SPACE)) || InputHandler::IsKeyHeld(SDLK_R) || initialAction == PointerAction::SecondaryMouseButton) { // TODO: Customizable for free unlike greedy ibispaint ver.14 initialRotation = Rotation; initialPanX = PanX; initialPanY = PanY; rotateStreams.push_back(stream); return true; } else if ((InputHandler::IsKeyHeld(SDLK_LCTRL) && InputHandler::IsKeyHeld(SDLK_SPACE)) || InputHandler::IsKeyHeld(SDLK_Z)) { initialZoom = Zoom; initialPanX = PanX; initialPanY = PanY; zoomStreams.push_back(stream); return true; } else if (InputHandler::IsKeyHeld(SDLK_SPACE) || initialAction == PointerAction::TertiaryMouseButton) { panStreams.push_back(stream); return true; } stream->ApplyCanvasTranslation(Zoom, PanX, PanY, Rotation, LayerManagementContext::FinalWidth, LayerManagementContext::FinalHeight); return Tool::GetActive()->OnDown(stream); } bool CanvasAppLayer::OnScroll(SDL_MouseWheelEvent e) { initialPanX = PanX; initialPanY = PanY; initialZoom = Zoom; // TODO: Redundant values double largestDimension = LayerManagementContext::FinalWidth > LayerManagementContext::FinalHeight ? LayerManagementContext::FinalWidth : LayerManagementContext::FinalHeight; double largestScreenDimension = App::Height > App::Width ? App::Height : App::Width; double saneZoomOut = 1.0f / largestDimension * largestScreenDimension / 4.0f; double saneZoomIn = 32.0f; // 3200% Zoom = initialZoom * SDL_exp(e.y / 8.0f); if (Zoom < saneZoomOut) Zoom = saneZoomOut; if (Zoom > saneZoomIn) Zoom = saneZoomIn; float anchorCanvasX = InputHandler::CursorX - (App::Width / 2.0f); float anchorCanvasY = -InputHandler::CursorY + (App::Height / 2.0f); // Keep anchor float zoomRatio = Zoom / initialZoom; PanX = anchorCanvasX - zoomRatio * (anchorCanvasX - initialPanX); PanY = anchorCanvasY - zoomRatio * (anchorCanvasY - initialPanY); NotifyAppLayer::SetMessage(std::to_string((int)(Zoom * 100)) + "%"); return true; } void CanvasAppLayer::OnRender() { preview = nullptr; if (Tool::GetActive()) { Tool::GetActive()->OnRender(); preview = Tool::GetActive()->GetPreview(); } if (panStreams.size() > 0) { float deltaX = 0, deltaY = 0; panStreams.front()->PullDeltaInput(&deltaX, &deltaY); PanX += deltaX; PanY += deltaY; float cosA = cosf(-Rotation); float sinA = sinf(-Rotation); double axisPanX = (PanX * cosA) - (PanY * sinA); double axisPanY = (PanX * sinA) + (PanY * cosA); NotifyAppLayer::SetMessage("X: " + std::to_string((int)SDL_floor(-axisPanX / Zoom)) + ", Y: " + std::to_string((int)SDL_floor(-axisPanY / Zoom))); if (panStreams.front()->GetComplete()) { panStreams.pop_front(); } } if (rotateStreams.size() > 0) { auto init = rotateStreams.front()->GetInitialInput(); auto latest = rotateStreams.front()->PullRawInputs().back(); // Rotate center float initialAngle = atan2f(init.screenY - (App::Height / 2.0f), init.screenX - (App::Width / 2.0f)); float latestAngle = atan2f(latest.screenY - (App::Height / 2.0f), latest.screenX - (App::Width / 2.0f)); float deltaAngle = latestAngle - initialAngle; Rotation = initialRotation - deltaAngle; // Adjust pan with angles (canvas rotation pivot is center) float cosA = cosf(-deltaAngle); float sinA = sinf(-deltaAngle); PanX = (initialPanX * cosA) - (initialPanY * sinA); PanY = (initialPanX * sinA) + (initialPanY * cosA); if (Rotation > M_PI) Rotation -= M_PI * 2.0f; if (Rotation < -M_PI) Rotation += M_PI * 2.0f; NotifyAppLayer::SetMessage(std::to_string((int)(Rotation * 180 / M_PI)) + "°"); if (rotateStreams.front()->GetComplete()) { rotateStreams.pop_front(); } } // sane limits double largestDimension = LayerManagementContext::FinalWidth > LayerManagementContext::FinalHeight ? LayerManagementContext::FinalWidth : LayerManagementContext::FinalHeight; double largestScreenDimension = App::Height > App::Width ? App::Height : App::Width; double saneZoomOut = 1.0f / largestDimension * largestScreenDimension / 4.0f; double saneZoomIn = 32.0f; // 3200% if (zoomStreams.size() > 0) { auto init = zoomStreams.front()->GetInitialInput(); auto latest = zoomStreams.front()->PullRawInputs().back(); // Zoom float drag = init.screenY - latest.screenY; float t = drag / App::Height; // normalized: -1..1 range roughly Zoom = initialZoom * SDL_exp(t * 2.0f); if (Zoom < saneZoomOut) Zoom = saneZoomOut; if (Zoom > saneZoomIn) Zoom = saneZoomIn; // Adjust pan with zoom (relative to center) float anchorCanvasX = init.screenX - (App::Width / 2.0f); float anchorCanvasY = -init.screenY + (App::Height / 2.0f); // Keep anchor float zoomRatio = Zoom / initialZoom; PanX = anchorCanvasX - zoomRatio * (anchorCanvasX - initialPanX); PanY = anchorCanvasY - zoomRatio * (anchorCanvasY - initialPanY); NotifyAppLayer::SetMessage(std::to_string((int)(Zoom * 100)) + "%"); if (zoomStreams.front()->GetComplete()) { zoomStreams.pop_front(); } } setPanLimited(); renderToSwapchain(); // TODO: implement preview cursor thing } void CanvasAppLayer::CreateCanvas(int width, int height) { SDL_GPUTextureCreateInfo creationInfo = {}; creationInfo.format = SDL_GPU_TEXTUREFORMAT_R8G8B8A8_UNORM; // TODO: make this configurable creationInfo.width = width; creationInfo.height = height; creationInfo.layer_count_or_depth = 1; creationInfo.num_levels = 1; creationInfo.sample_count = SDL_GPU_SAMPLECOUNT_1; creationInfo.usage = SDL_GPU_TEXTUREUSAGE_COLOR_TARGET | SDL_GPU_TEXTUREUSAGE_SAMPLER; localPreview = new Texture(creationInfo); SDL_SetGPUTextureName(GPU::GetDevice(), localPreview->Get(), "Canvas Preview // Scratch Texture"); } void CanvasAppLayer::OnDeactivate() { isActive = false; } double CanvasAppLayer::Zoom = 1.0; double CanvasAppLayer::PanX = 0.0; double CanvasAppLayer::PanY = 0.0; double CanvasAppLayer::Rotation = 0.0; // 90 degrees