/** * Viewport Server Implementation - Shared Memory Framebuffer Export * */ #include "viewport_server.h" #include #include #include #include #include #include #include #ifdef KORE_WINDOWS #include #endif #ifdef KORE_WINDOWS #include #else #include #include #include #include #include #endif #ifdef KORE_DIRECT3D11 #include #include // Forward declare structures from Kinc's Direct3D11.h #define MAXIMUM_WINDOWS 16 struct dx_window { HWND hwnd; IDXGISwapChain *swapChain; ID3D11Texture2D *backBuffer; ID3D11RenderTargetView *renderTargetView; ID3D11Texture2D *depthStencil; ID3D11DepthStencilView *depthStencilView; int width; int height; int new_width; int new_height; bool vsync; int depth_bits; int stencil_bits; }; struct dx_context { ID3D11Device *device; ID3D11DeviceContext *context; IDXGIDevice *dxgiDevice; IDXGIAdapter *dxgiAdapter; IDXGIFactory *dxgiFactory; int current_window; struct dx_window windows[MAXIMUM_WINDOWS]; }; // declare with C linkage to match Kinc C definition extern "C" struct dx_context dx_ctx; #endif // KORE_DIRECT3D11 #ifdef KORE_DIRECT3D12 #include #include #include // D3D12 uses Graphics5 layer - access device and command queue extern "C" ID3D12Device *device; extern "C" ID3D12CommandQueue *commandQueue; extern "C" ID3D12GraphicsCommandList *commandList; #endif // KORE_DIRECT3D12 #ifdef KORE_OPENGL #include #endif // KORE_OPENGL #ifdef KORE_VULKAN #include #include #endif // KORE_VULKAN // global viewport server state static ViewportServerState g_viewport_state = {0}; // ============================================================================ // Platform-specific Shared Memory Implementation // ============================================================================ #ifdef KORE_WINDOWS static bool shmem_create_windows(const char* name, size_t size) { // Create file mapping HANDLE hMapFile = CreateFileMappingA( INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, (DWORD)(size >> 32), (DWORD)(size & 0xFFFFFFFF), name ); if (hMapFile == NULL) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to create shared memory: %lu", GetLastError()); return false; } void* pMem = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, size); if (pMem == NULL) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to map shared memory: %lu", GetLastError()); CloseHandle(hMapFile); return false; } g_viewport_state.shmem_handle = hMapFile; g_viewport_state.shmem_ptr = pMem; g_viewport_state.shmem_size = size; SharedFramebufferHeader* header = (SharedFramebufferHeader*)pMem; memset(header, 0, sizeof(SharedFramebufferHeader)); header->magic = VIEWPORT_MAGIC; header->version = VIEWPORT_VERSION; header->width = g_viewport_state.width; header->height = g_viewport_state.height; header->frame_id = 0; header->ready_flag = 0; header->format = 0; // RGBA8 kinc_log(KINC_LOG_LEVEL_INFO, "Windows shared memory created: %s (%zu bytes)", name, size); return true; } static void shmem_destroy_windows(void) { if (g_viewport_state.shmem_ptr) { UnmapViewOfFile(g_viewport_state.shmem_ptr); g_viewport_state.shmem_ptr = NULL; } if (g_viewport_state.shmem_handle) { CloseHandle((HANDLE)g_viewport_state.shmem_handle); g_viewport_state.shmem_handle = NULL; } } #else static bool shmem_create_posix(const char* name, size_t size) { char shmem_name[256]; if (name[0] != '/') { snprintf(shmem_name, sizeof(shmem_name), "/%s", name); } else { strncpy(shmem_name, name, sizeof(shmem_name) - 1); } // remove already existing if any shm_unlink(shmem_name); // shared memory object int fd = shm_open(shmem_name, O_CREAT | O_RDWR, 0666); if (fd == -1) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to create shared memory: %s", strerror(errno)); return false; } if (ftruncate(fd, size) == -1) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to set shared memory size: %s", strerror(errno)); close(fd); shm_unlink(shmem_name); return false; } // map the memory void* pMem = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); if (pMem == MAP_FAILED) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to map shared memory: %s", strerror(errno)); close(fd); shm_unlink(shmem_name); return false; } g_viewport_state.shmem_handle = (void*)(intptr_t)fd; g_viewport_state.shmem_ptr = pMem; g_viewport_state.shmem_size = size; SharedFramebufferHeader* header = (SharedFramebufferHeader*)pMem; memset(header, 0, sizeof(SharedFramebufferHeader)); header->magic = VIEWPORT_MAGIC; header->version = VIEWPORT_VERSION; header->width = g_viewport_state.width; header->height = g_viewport_state.height; header->frame_id = 0; header->ready_flag = 0; header->format = 0; // RGBA8 kinc_log(KINC_LOG_LEVEL_INFO, "POSIX shared memory created: %s (%zu bytes)", shmem_name, size); return true; } static void shmem_destroy_posix(void) { if (g_viewport_state.shmem_ptr) { munmap(g_viewport_state.shmem_ptr, g_viewport_state.shmem_size); g_viewport_state.shmem_ptr = NULL; } if (g_viewport_state.shmem_handle) { close((int)(intptr_t)g_viewport_state.shmem_handle); g_viewport_state.shmem_handle = NULL; // unlink shared memory char shmem_name[256]; if (g_viewport_state.shmem_name[0] != '/') { snprintf(shmem_name, sizeof(shmem_name), "/%s", g_viewport_state.shmem_name); } else { strncpy(shmem_name, g_viewport_state.shmem_name, sizeof(shmem_name) - 1); } shm_unlink(shmem_name); } } #endif static bool shmem_create(const char* name, size_t size) { #ifdef KORE_WINDOWS return shmem_create_windows(name, size); #else return shmem_create_posix(name, size); #endif } static void shmem_destroy(void) { #ifdef KORE_WINDOWS shmem_destroy_windows(); #else shmem_destroy_posix(); #endif } bool viewport_server_init(const char* shmem_name, int width, int height) { if (g_viewport_state.initialized) { kinc_log(KINC_LOG_LEVEL_WARNING, "Viewport server already initialized"); return true; } if (width <= 0 || width > VIEWPORT_MAX_WIDTH || height <= 0 || height > VIEWPORT_MAX_HEIGHT) { kinc_log(KINC_LOG_LEVEL_ERROR, "Invalid viewport dimensions: %dx%d", width, height); return false; } g_viewport_state.width = width; g_viewport_state.height = height; strncpy(g_viewport_state.shmem_name, shmem_name ? shmem_name : VIEWPORT_SHMEM_NAME, sizeof(g_viewport_state.shmem_name) - 1); // pre-allocate shared memory for MAXIMUM size to avoid recreation on resize // *** this prevents access denied errors when Blender still has the memory mapped size_t max_pixel_size = (size_t)VIEWPORT_MAX_WIDTH * VIEWPORT_MAX_HEIGHT * 4; // RGBA8 size_t total_size = VIEWPORT_HEADER_SIZE + max_pixel_size; // create shared memory if (!shmem_create(g_viewport_state.shmem_name, total_size)) { return false; } // now allocate CPU side pixel buffer for readback with a max size for the resize support g_viewport_state.pixel_buffer = (uint8_t*)malloc(max_pixel_size); if (!g_viewport_state.pixel_buffer) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to allocate pixel buffer"); shmem_destroy(); return false; } // TODO: needs investigation // clear pixel buffer to prevent stale data from previous scenes memset(g_viewport_state.pixel_buffer, 0, max_pixel_size); // also clear shared memory pixel area to prevent flickering with old data uint8_t* pixel_dest = (uint8_t*)g_viewport_state.shmem_ptr + VIEWPORT_HEADER_SIZE; memset(pixel_dest, 0, max_pixel_size); kinc_g4_render_target_t* rt = (kinc_g4_render_target_t*)malloc(sizeof(kinc_g4_render_target_t)); if (!rt) { kinc_log(KINC_LOG_LEVEL_ERROR, "Failed to allocate render target"); free(g_viewport_state.pixel_buffer); shmem_destroy(); return false; } kinc_g4_render_target_init(rt, width, height, KINC_G4_RENDER_TARGET_FORMAT_32BIT, 24, 0); g_viewport_state.render_target = rt; g_viewport_state.enabled = true; g_viewport_state.initialized = true; g_viewport_state.frame_count = 0; kinc_log(KINC_LOG_LEVEL_INFO, "Viewport server initialized: %dx%d, shmem=%s", width, height, g_viewport_state.shmem_name); return true; } #ifdef KORE_DIRECT3D11 static ID3D11Texture2D* g_stagingTexture = NULL; static int g_stagingWidth = 0, g_stagingHeight = 0; #endif #ifdef KORE_DIRECT3D12 static ID3D12Resource* g_d3d12ReadbackBuffer = NULL; static size_t g_d3d12ReadbackSize = 0; #endif #ifdef KORE_VULKAN static VkBuffer g_vkReadbackBuffer = VK_NULL_HANDLE; static VkDeviceMemory g_vkReadbackMemory = VK_NULL_HANDLE; static size_t g_vkReadbackSize = 0; #endif void viewport_server_shutdown(void) { if (!g_viewport_state.initialized) { return; } #ifdef KORE_DIRECT3D11 if (g_stagingTexture) { g_stagingTexture->Release(); g_stagingTexture = NULL; g_stagingWidth = 0; g_stagingHeight = 0; } #endif #ifdef KORE_DIRECT3D12 if (g_d3d12ReadbackBuffer) { g_d3d12ReadbackBuffer->Release(); g_d3d12ReadbackBuffer = NULL; g_d3d12ReadbackSize = 0; } #endif #ifdef KORE_VULKAN if (g_vkReadbackBuffer != VK_NULL_HANDLE) { vkDestroyBuffer(vk_ctx.device, g_vkReadbackBuffer, NULL); g_vkReadbackBuffer = VK_NULL_HANDLE; } if (g_vkReadbackMemory != VK_NULL_HANDLE) { vkFreeMemory(vk_ctx.device, g_vkReadbackMemory, NULL); g_vkReadbackMemory = VK_NULL_HANDLE; } g_vkReadbackSize = 0; #endif if (g_viewport_state.render_target) { kinc_g4_render_target_destroy((kinc_g4_render_target_t*)g_viewport_state.render_target); free(g_viewport_state.render_target); g_viewport_state.render_target = NULL; } if (g_viewport_state.pixel_buffer) { free(g_viewport_state.pixel_buffer); g_viewport_state.pixel_buffer = NULL; } shmem_destroy(); g_viewport_state.enabled = false; g_viewport_state.initialized = false; g_viewport_state.frame_count = 0; kinc_log(KINC_LOG_LEVEL_INFO, "Viewport server shutdown complete"); } bool viewport_server_is_enabled(void) { return g_viewport_state.enabled && g_viewport_state.initialized; } void viewport_server_begin_frame(void) { // no-op we let iron render normally to framebuffer and capture the backbuffer in end_frame BEFORE swap_buffers } void viewport_server_end_frame(void) { if (!viewport_server_is_enabled()) return; if (g_viewport_state.pixel_buffer == NULL || g_viewport_state.shmem_ptr == NULL) { kinc_log(KINC_LOG_LEVEL_WARNING, "Viewport server: invalid state in end_frame"); g_viewport_state.frame_count++; return; } if (g_viewport_state.frame_count < 3) { g_viewport_state.frame_count++; return; } SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; header->width = g_viewport_state.width; header->height = g_viewport_state.height; size_t pixel_size = (size_t)g_viewport_state.width * g_viewport_state.height * 4; uint8_t* pixels = g_viewport_state.pixel_buffer; uint8_t* pixel_dest = (uint8_t*)g_viewport_state.shmem_ptr + VIEWPORT_HEADER_SIZE; // ==================================================================== // Backend-specific framebuffer readback // ==================================================================== #if defined(KORE_DIRECT3D11) // --- Direct3D 11: copy backbuffer to staging texture, then map --- { ID3D11DeviceContext* ctx = dx_ctx.context; ID3D11Device* dev = dx_ctx.device; struct dx_window* win = &dx_ctx.windows[0]; ID3D11Texture2D* backBuffer = win->backBuffer; if (backBuffer && ctx && dev) { D3D11_TEXTURE2D_DESC bbDesc; backBuffer->GetDesc(&bbDesc); int bbWidth = (int)bbDesc.Width; int bbHeight = (int)bbDesc.Height; // capture dimensions are the actual size of whats renderedso we clamp to max shared memory size int captureWidth = bbWidth; int captureHeight = bbHeight; if (captureWidth > VIEWPORT_MAX_WIDTH) captureWidth = VIEWPORT_MAX_WIDTH; if (captureHeight > VIEWPORT_MAX_HEIGHT) captureHeight = VIEWPORT_MAX_HEIGHT; header->width = captureWidth; header->height = captureHeight; pixel_size = (size_t)captureWidth * captureHeight * 4; // same dimensions between source and destination if (!g_stagingTexture || g_stagingWidth != bbWidth || g_stagingHeight != bbHeight) { if (g_stagingTexture) g_stagingTexture->Release(); D3D11_TEXTURE2D_DESC desc = {}; desc.Width = bbWidth; desc.Height = bbHeight; desc.MipLevels = 1; desc.ArraySize = 1; desc.Format = bbDesc.Format; desc.SampleDesc.Count = 1; desc.Usage = D3D11_USAGE_STAGING; desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ; HRESULT hr = dev->CreateTexture2D(&desc, NULL, &g_stagingTexture); if (FAILED(hr)) { kinc_log(KINC_LOG_LEVEL_ERROR, "D3D11: Failed to create staging texture: 0x%08X", hr); g_stagingTexture = NULL; g_stagingWidth = 0; g_stagingHeight = 0; g_viewport_state.frame_count++; return; } g_stagingWidth = bbWidth; g_stagingHeight = bbHeight; } ctx->CopyResource(g_stagingTexture, backBuffer); // only read the portion we need D3D11_MAPPED_SUBRESOURCE mapped; if (SUCCEEDED(ctx->Map(g_stagingTexture, 0, D3D11_MAP_READ, 0, &mapped))) { for (int y = 0; y < captureHeight; y++) { memcpy(pixels + y * captureWidth * 4, (uint8_t*)mapped.pData + y * mapped.RowPitch, captureWidth * 4); } ctx->Unmap(g_stagingTexture, 0); // BGRA to RGBA conversion for (size_t i = 0; i < pixel_size; i += 4) { uint8_t t = pixels[i]; pixels[i] = pixels[i+2]; pixels[i+2] = t; } memcpy(pixel_dest, pixels, pixel_size); } } } #elif defined(KORE_DIRECT3D12) { int captureWidth = g_viewport_state.width; int captureHeight = g_viewport_state.height; if (captureWidth > VIEWPORT_MAX_WIDTH) captureWidth = VIEWPORT_MAX_WIDTH; if (captureHeight > VIEWPORT_MAX_HEIGHT) captureHeight = VIEWPORT_MAX_HEIGHT; header->width = captureWidth; header->height = captureHeight; // aligned to D3D12_TEXTURE_DATA_PITCH_ALIGNMENT (256 bytes) UINT rowPitch = (captureWidth * 4 + D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1) & ~(D3D12_TEXTURE_DATA_PITCH_ALIGNMENT - 1); size_t readbackSize = (size_t)rowPitch * captureHeight; pixel_size = (size_t)captureWidth * captureHeight * 4; if (!g_d3d12ReadbackBuffer || g_d3d12ReadbackSize < readbackSize) { if (g_d3d12ReadbackBuffer) g_d3d12ReadbackBuffer->Release(); D3D12_HEAP_PROPERTIES heapProps = {}; heapProps.Type = D3D12_HEAP_TYPE_READBACK; D3D12_RESOURCE_DESC bufDesc = {}; bufDesc.Dimension = D3D12_RESOURCE_DIMENSION_BUFFER; bufDesc.Width = readbackSize; bufDesc.Height = 1; bufDesc.DepthOrArraySize = 1; bufDesc.MipLevels = 1; bufDesc.SampleDesc.Count = 1; bufDesc.Layout = D3D12_TEXTURE_LAYOUT_ROW_MAJOR; HRESULT hr = device->CreateCommittedResource( &heapProps, D3D12_HEAP_FLAG_NONE, &bufDesc, D3D12_RESOURCE_STATE_COPY_DEST, NULL, IID_PPV_ARGS(&g_d3d12ReadbackBuffer)); if (FAILED(hr)) { kinc_log(KINC_LOG_LEVEL_ERROR, "D3D12: Failed to create readback buffer: 0x%08X", hr); g_d3d12ReadbackBuffer = NULL; g_d3d12ReadbackSize = 0; g_viewport_state.frame_count++; return; } g_d3d12ReadbackSize = readbackSize; } ID3D12CommandAllocator* copyAllocator = NULL; ID3D12GraphicsCommandList* copyCmd = NULL; HRESULT hr = device->CreateCommandAllocator(D3D12_COMMAND_LIST_TYPE_DIRECT, IID_PPV_ARGS(©Allocator)); if (SUCCEEDED(hr)) { hr = device->CreateCommandList(0, D3D12_COMMAND_LIST_TYPE_DIRECT, copyAllocator, NULL, IID_PPV_ARGS(©Cmd)); } if (SUCCEEDED(hr) && copyCmd) { extern struct dx_window dx_windows[]; struct dx_window* win = &dx_windows[0]; ID3D12Resource* backBuffer = NULL; if (win->swapChain) { IDXGISwapChain3* swapChain3 = NULL; if (SUCCEEDED(((IDXGISwapChain*)win->swapChain)->QueryInterface(IID_PPV_ARGS(&swapChain3)))) { UINT backBufferIndex = swapChain3->GetCurrentBackBufferIndex(); swapChain3->GetBuffer(backBufferIndex, IID_PPV_ARGS(&backBuffer)); swapChain3->Release(); } } if (backBuffer) { D3D12_RESOURCE_BARRIER barrier = {}; barrier.Type = D3D12_RESOURCE_BARRIER_TYPE_TRANSITION; barrier.Transition.pResource = backBuffer; barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_RENDER_TARGET; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_COPY_SOURCE; barrier.Transition.Subresource = D3D12_RESOURCE_BARRIER_ALL_SUBRESOURCES; copyCmd->ResourceBarrier(1, &barrier); D3D12_TEXTURE_COPY_LOCATION srcLoc = {}; srcLoc.pResource = backBuffer; srcLoc.Type = D3D12_TEXTURE_COPY_TYPE_SUBRESOURCE_INDEX; srcLoc.SubresourceIndex = 0; D3D12_TEXTURE_COPY_LOCATION dstLoc = {}; dstLoc.pResource = g_d3d12ReadbackBuffer; dstLoc.Type = D3D12_TEXTURE_COPY_TYPE_PLACED_FOOTPRINT; dstLoc.PlacedFootprint.Offset = 0; dstLoc.PlacedFootprint.Footprint.Format = DXGI_FORMAT_R8G8B8A8_UNORM; dstLoc.PlacedFootprint.Footprint.Width = captureWidth; dstLoc.PlacedFootprint.Footprint.Height = captureHeight; dstLoc.PlacedFootprint.Footprint.Depth = 1; dstLoc.PlacedFootprint.Footprint.RowPitch = rowPitch; copyCmd->CopyTextureRegion(&dstLoc, 0, 0, 0, &srcLoc, NULL); barrier.Transition.StateBefore = D3D12_RESOURCE_STATE_COPY_SOURCE; barrier.Transition.StateAfter = D3D12_RESOURCE_STATE_RENDER_TARGET; copyCmd->ResourceBarrier(1, &barrier); copyCmd->Close(); ID3D12CommandList* lists[] = { copyCmd }; commandQueue->ExecuteCommandLists(1, lists); ID3D12Fence* fence = NULL; HANDLE event = CreateEvent(NULL, FALSE, FALSE, NULL); if (SUCCEEDED(device->CreateFence(0, D3D12_FENCE_FLAG_NONE, IID_PPV_ARGS(&fence)))) { commandQueue->Signal(fence, 1); fence->SetEventOnCompletion(1, event); WaitForSingleObject(event, 1000); fence->Release(); } CloseHandle(event); void* mappedData = NULL; D3D12_RANGE readRange = { 0, readbackSize }; if (SUCCEEDED(g_d3d12ReadbackBuffer->Map(0, &readRange, &mappedData))) { for (int y = 0; y < captureHeight; y++) { memcpy(pixels + y * captureWidth * 4, (uint8_t*)mappedData + y * rowPitch, captureWidth * 4); } D3D12_RANGE writeRange = { 0, 0 }; g_d3d12ReadbackBuffer->Unmap(0, &writeRange); memcpy(pixel_dest, pixels, pixel_size); } backBuffer->Release(); } } if (copyCmd) copyCmd->Release(); if (copyAllocator) copyAllocator->Release(); } #elif defined(KORE_OPENGL) { int captureWidth = g_viewport_state.width; int captureHeight = g_viewport_state.height; if (captureWidth > VIEWPORT_MAX_WIDTH) captureWidth = VIEWPORT_MAX_WIDTH; if (captureHeight > VIEWPORT_MAX_HEIGHT) captureHeight = VIEWPORT_MAX_HEIGHT; header->width = captureWidth; header->height = captureHeight; pixel_size = (size_t)captureWidth * captureHeight * 4; glBindFramebuffer(GL_READ_FRAMEBUFFER, 0); glReadPixels(0, 0, captureWidth, captureHeight, GL_RGBA, GL_UNSIGNED_BYTE, pixels); int stride = captureWidth * 4; uint8_t* row_temp = (uint8_t*)alloca(stride); for (int y = 0; y < captureHeight / 2; y++) { uint8_t* top = pixels + y * stride; uint8_t* bot = pixels + (captureHeight - 1 - y) * stride; memcpy(row_temp, top, stride); memcpy(top, bot, stride); memcpy(bot, row_temp, stride); } memcpy(pixel_dest, pixels, pixel_size); } #elif defined(KORE_VULKAN) { int captureWidth = g_viewport_state.width; int captureHeight = g_viewport_state.height; if (captureWidth > VIEWPORT_MAX_WIDTH) captureWidth = VIEWPORT_MAX_WIDTH; if (captureHeight > VIEWPORT_MAX_HEIGHT) captureHeight = VIEWPORT_MAX_HEIGHT; header->width = captureWidth; header->height = captureHeight; pixel_size = (size_t)captureWidth * captureHeight * 4; if (g_vkReadbackBuffer == VK_NULL_HANDLE || g_vkReadbackSize < pixel_size) { if (g_vkReadbackBuffer != VK_NULL_HANDLE) { vkDestroyBuffer(vk_ctx.device, g_vkReadbackBuffer, NULL); vkFreeMemory(vk_ctx.device, g_vkReadbackMemory, NULL); } VkBufferCreateInfo bufInfo = {}; bufInfo.sType = VK_STRUCTURE_TYPE_BUFFER_CREATE_INFO; bufInfo.size = pixel_size; bufInfo.usage = VK_BUFFER_USAGE_TRANSFER_DST_BIT; bufInfo.sharingMode = VK_SHARING_MODE_EXCLUSIVE; if (vkCreateBuffer(vk_ctx.device, &bufInfo, NULL, &g_vkReadbackBuffer) != VK_SUCCESS) { kinc_log(KINC_LOG_LEVEL_ERROR, "Vulkan: Failed to create readback buffer"); g_vkReadbackBuffer = VK_NULL_HANDLE; g_vkReadbackSize = 0; g_viewport_state.frame_count++; return; } VkMemoryRequirements memReqs; vkGetBufferMemoryRequirements(vk_ctx.device, g_vkReadbackBuffer, &memReqs); uint32_t memTypeIndex = UINT32_MAX; for (uint32_t i = 0; i < vk_ctx.memory_properties.memoryTypeCount; i++) { if ((memReqs.memoryTypeBits & (1 << i)) && (vk_ctx.memory_properties.memoryTypes[i].propertyFlags & (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) == (VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_HOST_COHERENT_BIT)) { memTypeIndex = i; break; } } if (memTypeIndex == UINT32_MAX) { kinc_log(KINC_LOG_LEVEL_ERROR, "Vulkan: No suitable memory type for readback"); vkDestroyBuffer(vk_ctx.device, g_vkReadbackBuffer, NULL); g_vkReadbackBuffer = VK_NULL_HANDLE; g_vkReadbackSize = 0; g_viewport_state.frame_count++; return; } VkMemoryAllocateInfo allocInfo = {}; allocInfo.sType = VK_STRUCTURE_TYPE_MEMORY_ALLOCATE_INFO; allocInfo.allocationSize = memReqs.size; allocInfo.memoryTypeIndex = memTypeIndex; if (vkAllocateMemory(vk_ctx.device, &allocInfo, NULL, &g_vkReadbackMemory) != VK_SUCCESS) { kinc_log(KINC_LOG_LEVEL_ERROR, "Vulkan: Failed to allocate readback memory"); vkDestroyBuffer(vk_ctx.device, g_vkReadbackBuffer, NULL); g_vkReadbackBuffer = VK_NULL_HANDLE; g_vkReadbackSize = 0; g_viewport_state.frame_count++; return; } vkBindBufferMemory(vk_ctx.device, g_vkReadbackBuffer, g_vkReadbackMemory, 0); g_vkReadbackSize = pixel_size; } struct vk_window* win = &vk_ctx.windows[0]; if (win->images && win->current_image < win->image_count) { VkImage swapImage = win->images[win->current_image]; VkCommandBufferAllocateInfo cmdAllocInfo = {}; cmdAllocInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_ALLOCATE_INFO; cmdAllocInfo.commandPool = vk_ctx.cmd_pool; cmdAllocInfo.level = VK_COMMAND_BUFFER_LEVEL_PRIMARY; cmdAllocInfo.commandBufferCount = 1; VkCommandBuffer cmdBuf; if (vkAllocateCommandBuffers(vk_ctx.device, &cmdAllocInfo, &cmdBuf) != VK_SUCCESS) { g_viewport_state.frame_count++; return; } VkCommandBufferBeginInfo beginInfo = {}; beginInfo.sType = VK_STRUCTURE_TYPE_COMMAND_BUFFER_BEGIN_INFO; beginInfo.flags = VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT; vkBeginCommandBuffer(cmdBuf, &beginInfo); VkImageMemoryBarrier barrier = {}; barrier.sType = VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER; barrier.oldLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; barrier.newLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.srcAccessMask = VK_ACCESS_MEMORY_READ_BIT; barrier.dstAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.image = swapImage; barrier.subresourceRange.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; barrier.subresourceRange.baseMipLevel = 0; barrier.subresourceRange.levelCount = 1; barrier.subresourceRange.baseArrayLayer = 0; barrier.subresourceRange.layerCount = 1; vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, VK_PIPELINE_STAGE_TRANSFER_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); VkBufferImageCopy region = {}; region.bufferOffset = 0; region.bufferRowLength = 0; region.bufferImageHeight = 0; region.imageSubresource.aspectMask = VK_IMAGE_ASPECT_COLOR_BIT; region.imageSubresource.mipLevel = 0; region.imageSubresource.baseArrayLayer = 0; region.imageSubresource.layerCount = 1; region.imageOffset = {0, 0, 0}; region.imageExtent = {(uint32_t)captureWidth, (uint32_t)captureHeight, 1}; vkCmdCopyImageToBuffer(cmdBuf, swapImage, VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL, g_vkReadbackBuffer, 1, ®ion); barrier.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_SRC_OPTIMAL; barrier.newLayout = VK_IMAGE_LAYOUT_PRESENT_SRC_KHR; barrier.srcAccessMask = VK_ACCESS_TRANSFER_READ_BIT; barrier.dstAccessMask = VK_ACCESS_MEMORY_READ_BIT; vkCmdPipelineBarrier(cmdBuf, VK_PIPELINE_STAGE_TRANSFER_BIT, VK_PIPELINE_STAGE_ALL_COMMANDS_BIT, 0, 0, NULL, 0, NULL, 1, &barrier); vkEndCommandBuffer(cmdBuf); VkSubmitInfo submitInfo = {}; submitInfo.sType = VK_STRUCTURE_TYPE_SUBMIT_INFO; submitInfo.commandBufferCount = 1; submitInfo.pCommandBuffers = &cmdBuf; vkQueueSubmit(vk_ctx.queue, 1, &submitInfo, VK_NULL_HANDLE); vkQueueWaitIdle(vk_ctx.queue); vkFreeCommandBuffers(vk_ctx.device, vk_ctx.cmd_pool, 1, &cmdBuf); void* mappedData = NULL; if (vkMapMemory(vk_ctx.device, g_vkReadbackMemory, 0, pixel_size, 0, &mappedData) == VK_SUCCESS) { memcpy(pixels, mappedData, pixel_size); vkUnmapMemory(vk_ctx.device, g_vkReadbackMemory); if (win->format.format == VK_FORMAT_B8G8R8A8_UNORM || win->format.format == VK_FORMAT_B8G8R8A8_SRGB) { for (size_t i = 0; i < pixel_size; i += 4) { uint8_t t = pixels[i]; pixels[i] = pixels[i+2]; pixels[i+2] = t; } } memcpy(pixel_dest, pixels, pixel_size); } } } #else // other platforms use render target if (g_viewport_state.render_target) { kinc_g4_render_target_t* rt = (kinc_g4_render_target_t*)g_viewport_state.render_target; kinc_g4_render_target_get_pixels(rt, pixels); memcpy(pixel_dest, pixels, pixel_size); } #endif g_viewport_state.frame_count++; header->frame_id = g_viewport_state.frame_count; // NOTE: Camera sync from RunT to Blender is handled via viewport_server_set_camera() // which is called explicitly when RunT's internal camera changes. // We do NOT extract camera from view_matrix here as that would create a feedback loop // (Blender sends view_matrix -> we extract camera -> Blender applies it -> repeat) // Memory barrier to ensure writes complete before setting ready flag #ifdef KORE_WINDOWS MemoryBarrier(); #else __sync_synchronize(); #endif header->ready_flag = 1; } bool viewport_server_check_resize(int* new_width, int* new_height) { if (!viewport_server_is_enabled()) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; if (header->resize_request) { *new_width = header->viewport_width; *new_height = header->viewport_height; header->resize_request = 0; return true; } return false; } bool viewport_server_resize(int new_width, int new_height) { if (!g_viewport_state.initialized) return false; if (new_width <= 0 || new_width > VIEWPORT_MAX_WIDTH || new_height <= 0 || new_height > VIEWPORT_MAX_HEIGHT) { kinc_log(KINC_LOG_LEVEL_ERROR, "Invalid resize dimensions: %dx%d", new_width, new_height); return false; } if (new_width == g_viewport_state.width && new_height == g_viewport_state.height) { return true; } kinc_log(KINC_LOG_LEVEL_INFO, "Viewport server resizing: %dx%d -> %dx%d", g_viewport_state.width, g_viewport_state.height, new_width, new_height); // resize the actual Kinc window so the D3D11 backbuffer is the correct size critical because viewport_server_end_frame captures from the backbuffer kinc_window_resize(0, new_width, new_height); if (g_viewport_state.render_target) { kinc_g4_render_target_destroy((kinc_g4_render_target_t*)g_viewport_state.render_target); // dont free here we reuse the allocation } // update dimensions pre-allocated and reinitialize render target with new size using same memory g_viewport_state.width = new_width; g_viewport_state.height = new_height; kinc_g4_render_target_t* rt = (kinc_g4_render_target_t*)g_viewport_state.render_target; if (rt) { kinc_g4_render_target_init(rt, new_width, new_height, KINC_G4_RENDER_TARGET_FORMAT_32BIT, 24, 0); } SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; if (header) { header->width = new_width; header->height = new_height; header->ready_flag = 0; } kinc_log(KINC_LOG_LEVEL_INFO, "Viewport server resize complete"); return true; } bool viewport_server_check_shutdown(void) { if (!viewport_server_is_enabled()) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; return header->shutdown_flag != 0; } bool viewport_server_get_view_matrix(float* matrix) { if (!viewport_server_is_enabled() || !matrix) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; memcpy(matrix, header->view_matrix, 16 * sizeof(float)); return true; } bool viewport_server_get_proj_matrix(float* matrix) { if (!viewport_server_is_enabled() || !matrix) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; memcpy(matrix, header->proj_matrix, 16 * sizeof(float)); return true; } ViewportServerState* viewport_server_get_state(void) { return &g_viewport_state; } void viewport_server_set_camera(float pos_x, float pos_y, float pos_z, float rot_x, float rot_y, float rot_z, float rot_w) { if (!viewport_server_is_enabled()) return; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; if (!header) return; header->runt_camera_pos[0] = pos_x; header->runt_camera_pos[1] = pos_y; header->runt_camera_pos[2] = pos_z; header->runt_camera_rot[0] = rot_x; header->runt_camera_rot[1] = rot_y; header->runt_camera_rot[2] = rot_z; header->runt_camera_rot[3] = rot_w; header->runt_camera_dirty = 1; } bool viewport_server_read_input(InputEvent* event) { if (!viewport_server_is_enabled()) return false; if (!event) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; if (!header) return false; uint32_t read_idx = header->input_read_idx; uint32_t write_idx = header->input_write_idx; if (read_idx == write_idx) { return false; } *event = header->input_events[read_idx % MAX_INPUT_EVENTS]; header->input_read_idx = (read_idx + 1) % MAX_INPUT_EVENTS; return true; } bool viewport_server_input_enabled(void) { if (!viewport_server_is_enabled()) return false; SharedFramebufferHeader* header = (SharedFramebufferHeader*)g_viewport_state.shmem_ptr; if (!header) return false; return header->input_enabled != 0; }