| /* | |
| * Copyright 2026 Rive | |
| * | |
| * GM test for Ore's texture → view → pipeline → render path. | |
| * | |
| * Creates a small RGBA checkerboard texture via ore::Context::makeTexture(), | |
| * wraps it in an ore::TextureView, builds a fullscreen-quad pipeline that | |
| * samples the texture, renders to a RenderCanvas, and composites into the | |
| * main framebuffer. | |
| * | |
| * If the image appears as a 4×4 checkerboard (red/blue squares), Ore's | |
| * texture and render-pipeline integration is working for this backend. | |
| */ | |
| #include "gm.hpp" | |
| #include "gmutils.hpp" | |
| #include "ore_gm_helper.hpp" | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #include "rive/renderer/render_canvas.hpp" | |
| #include "rive/renderer/ore/ore_buffer.hpp" | |
| #include "rive/renderer/ore/ore_texture.hpp" | |
| #include "rive/renderer/ore/ore_sampler.hpp" | |
| #include "rive/renderer/ore/ore_bind_group.hpp" | |
| #include "rive/renderer/ore/ore_shader_module.hpp" | |
| #include "rive/renderer/ore/ore_pipeline.hpp" | |
| #include "rive/renderer/ore/ore_render_pass.hpp" | |
| #endif | |
| using namespace rivegm; | |
| using namespace rive; | |
| using namespace rive::gpu; | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| using namespace rive::ore; | |
| #endif | |
| // 4×4 RGBA8 checkerboard: red (0xFF0000FF) and blue (0x0000FFFF). | |
| static void make4x4Checkerboard(uint8_t pixels[4 * 4 * 4]) | |
| { | |
| for (int y = 0; y < 4; y++) | |
| { | |
| for (int x = 0; x < 4; x++) | |
| { | |
| int i = (y * 4 + x) * 4; | |
| bool isRed = ((x + y) % 2 == 0); | |
| pixels[i + 0] = isRed ? 255 : 0; // R | |
| pixels[i + 1] = 0; // G | |
| pixels[i + 2] = isRed ? 0 : 255; // B | |
| pixels[i + 3] = 255; // A | |
| } | |
| } | |
| } | |
| #if defined(ORE_BACKEND_METAL) || defined(ORE_BACKEND_GL) || \ | |
| defined(ORE_BACKEND_VK) || defined(ORE_BACKEND_WGPU) || \ | |
| defined(ORE_BACKEND_D3D11) || defined(ORE_BACKEND_D3D12) || \ | |
| defined(ORE_BACKEND_RHI) | |
| #define ORE_IMAGE_VIEW_ACTIVE | |
| #endif | |
| class OreImageViewGM : public GM | |
| { | |
| public: | |
| OreImageViewGM() : GM(256, 256) {} | |
| ColorInt clearColor() const override { return 0xff000000; } | |
| void onDraw(rive::Renderer* originalRenderer) override | |
| { | |
| auto renderContext = TestingWindow::Get()->renderContext(); | |
| if (!renderContext || !m_ore.ensureContext(renderContext)) | |
| return; | |
| #ifdef ORE_IMAGE_VIEW_ACTIVE | |
| auto& ctx = *renderContext->getOreContext(); | |
| // ── 1. Create a 4×4 test image as an ore texture. | |
| uint8_t pixels[4 * 4 * 4]; | |
| make4x4Checkerboard(pixels); | |
| TextureDesc imgTexDesc{}; | |
| imgTexDesc.width = 4; | |
| imgTexDesc.height = 4; | |
| imgTexDesc.format = TextureFormat::rgba8unorm; | |
| imgTexDesc.type = TextureType::texture2D; | |
| imgTexDesc.numMipmaps = 1; | |
| imgTexDesc.sampleCount = 1; | |
| imgTexDesc.label = "ore_image_view_checkerboard"; | |
| auto oreTex = ctx.makeTexture(imgTexDesc); | |
| if (!oreTex) | |
| return; | |
| TextureDataDesc uploadDesc{}; | |
| uploadDesc.data = pixels; | |
| uploadDesc.width = 4; | |
| uploadDesc.height = 4; | |
| uploadDesc.bytesPerRow = 4 * 4; // rgba8unorm = 4 bytes per pixel | |
| uploadDesc.rowsPerImage = 4; | |
| // ── 2. Create a view of the texture ── | |
| TextureViewDesc tvDesc{}; | |
| tvDesc.texture = oreTex.get(); | |
| tvDesc.dimension = TextureViewDimension::texture2D; | |
| tvDesc.baseMipLevel = 0; | |
| tvDesc.mipCount = 1; | |
| tvDesc.baseLayer = 0; | |
| tvDesc.layerCount = 1; | |
| auto texView = ctx.makeTextureView(tvDesc); | |
| if (!texView) | |
| return; | |
| // ── 3. Create sampler (nearest for crisp checkerboard) ── | |
| SamplerDesc sampDesc{}; | |
| sampDesc.minFilter = Filter::nearest; | |
| sampDesc.magFilter = Filter::nearest; | |
| auto sampler = ctx.makeSampler(sampDesc); | |
| // ── 4. Load shader from the pre-compiled RSTB ── | |
| auto shader = ore_gm::loadShader(ctx, ore_gm::kImageView); | |
| if (!shader.vsModule) | |
| return; | |
| // ── 5. Create pipeline (no vertex buffers — fullscreen quad from | |
| // vertex_index) ── | |
| auto layout1 = | |
| ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 1); | |
| auto layout2 = | |
| ore_gm::makeLayoutFromShader(ctx, shader.vsModule.get(), 2); | |
| BindGroupLayout* layouts[] = {nullptr, layout1.get(), layout2.get()}; | |
| PipelineDesc pipeDesc{}; | |
| pipeDesc.vertexModule = shader.vsModule.get(); | |
| pipeDesc.fragmentModule = shader.psModule.get(); | |
| pipeDesc.vertexEntryPoint = shader.vsEntryPoint; | |
| pipeDesc.fragmentEntryPoint = shader.fsEntryPoint; | |
| pipeDesc.vertexBufferCount = 0; | |
| pipeDesc.topology = PrimitiveTopology::triangleList; | |
| pipeDesc.colorTargets[0].format = TextureFormat::rgba8unorm; | |
| pipeDesc.colorCount = 1; | |
| pipeDesc.depthStencil.depthCompare = CompareFunction::always; | |
| pipeDesc.depthStencil.depthWriteEnabled = false; | |
| pipeDesc.bindGroupLayouts = layouts; | |
| pipeDesc.bindGroupLayoutCount = 3; | |
| pipeDesc.label = "ore_image_view_pipeline"; | |
| auto pipeline = ctx.makePipeline(pipeDesc); | |
| if (!pipeline) | |
| { | |
| fprintf(stderr, | |
| "[ore_image_view] pipeline creation failed: %s\n", | |
| ctx.lastError().c_str()); | |
| return; | |
| } | |
| // ── 6. Create bind groups for texture + sampler ── | |
| BindGroupDesc texBGDesc{}; | |
| texBGDesc.layout = layout1.get(); | |
| BindGroupDesc::TexEntry texEntry{}; | |
| texEntry.slot = 0; | |
| texEntry.view = texView.get(); | |
| texBGDesc.textures = &texEntry; | |
| texBGDesc.textureCount = 1; | |
| auto texBG = ctx.makeBindGroup(texBGDesc); | |
| BindGroupDesc sampBGDesc{}; | |
| sampBGDesc.layout = layout2.get(); | |
| BindGroupDesc::SampEntry sampEntry{}; | |
| sampEntry.slot = 0; | |
| sampEntry.sampler = sampler.get(); | |
| sampBGDesc.samplers = &sampEntry; | |
| sampBGDesc.samplerCount = 1; | |
| auto sampBG = ctx.makeBindGroup(sampBGDesc); | |
| // ── 7. Render into a RenderCanvas ── | |
| auto canvas = renderContext->makeRenderCanvas(256, 256); | |
| if (!canvas) | |
| return; | |
| auto canvasTarget = ctx.wrapCanvasTexture(canvas.get()); | |
| if (!canvasTarget) | |
| return; | |
| m_ore.beginFrame(renderContext); | |
| // Upload must happen after beginFrame() so the command buffer is | |
| // in the recording state. | |
| oreTex->upload(uploadDesc); | |
| ColorAttachment ca{}; | |
| ca.view = canvasTarget.get(); | |
| ca.loadOp = LoadOp::clear; | |
| ca.storeOp = StoreOp::store; | |
| ca.clearColor = {0, 0, 0, 1}; | |
| RenderPassDesc rpDesc{}; | |
| rpDesc.colorAttachments[0] = ca; | |
| rpDesc.colorCount = 1; | |
| rpDesc.label = "ore_image_view_pass"; | |
| auto pass = ctx.beginRenderPass(rpDesc); | |
| pass->setPipeline(pipeline.get()); | |
| pass->setBindGroup(1, texBG.get()); | |
| pass->setBindGroup(2, sampBG.get()); | |
| pass->setViewport(0, 0, 256, 256); | |
| pass->draw(6); // fullscreen quad — 2 triangles | |
| pass->finish(); | |
| m_ore.endFrame(renderContext); | |
| ore_gm::invalidateGLStateAfterOre(renderContext); | |
| // ── 8. Composite into main framebuffer ── | |
| originalRenderer->save(); | |
| originalRenderer->drawImage(canvas->renderImage(), | |
| {.filter = ImageFilter::nearest}, | |
| BlendMode::srcOver, | |
| 1); | |
| originalRenderer->restore(); | |
| #endif // ORE_IMAGE_VIEW_ACTIVE | |
| } | |
| private: | |
| ore_gm::OreGMContext m_ore; | |
| }; | |
| GMREGISTER(ore_image_view, return new OreImageViewGM) |