diff --git a/.github/workflows/screenshot-tests.yml b/.github/workflows/screenshot-tests.yml new file mode 100644 index 00000000..bee5a984 --- /dev/null +++ b/.github/workflows/screenshot-tests.yml @@ -0,0 +1,87 @@ +name: Screenshot Tests + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +concurrency: + group: screenshot-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + screenshot-report: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up JDK 25 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '25' + cache: gradle + + - name: Grant Gradle wrapper execute permission + run: chmod +x gradlew + + - name: Install Mesa software rendering and virtual display + run: | + sudo apt-get update + sudo apt-get install -y \ + mesa-utils \ + libegl-mesa0 \ + libgl1-mesa-dri \ + libglx-mesa0 \ + libvulkan1 \ + mesa-vulkan-drivers \ + xvfb \ + libjemalloc2 + + - name: Run screenshot tests (samples) with software rendering + run: | + export LIBGL_ALWAYS_SOFTWARE=1 + export MESA_GL_VERSION_OVERRIDE=4.5 + export MESA_GLSL_VERSION_OVERRIDE=450 + export GALLIUM_DRIVER=llvmpipe + export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json + xvfb-run -a -s "-screen 0 1280x720x24" \ + ./gradlew :samples:tests:screenshot:screenshotReport --no-daemon --continue \ + || true + + - name: Run screenshot tests (examples) with software rendering + run: | + export LIBGL_ALWAYS_SOFTWARE=1 + export MESA_GL_VERSION_OVERRIDE=4.5 + export MESA_GLSL_VERSION_OVERRIDE=450 + export GALLIUM_DRIVER=llvmpipe + export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.x86_64.json + xvfb-run -a -s "-screen 0 1280x720x24" \ + ./gradlew :examples:screenshotReport --no-daemon --continue \ + || true + + - name: Upload screenshot report (samples) + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshot-report-samples + path: | + samples/tests/screenshot/build/screenshots/ + samples/tests/screenshot/build/test-results/ + retention-days: 30 + + - name: Upload screenshot report (examples) + if: always() + uses: actions/upload-artifact@v4 + with: + name: screenshot-report-examples + path: | + examples/build/screenshots/ + examples/build/test-results/ + retention-days: 30 diff --git a/TODO.md b/TODO.md index 4a7b80d9..e79d112b 100644 --- a/TODO.md +++ b/TODO.md @@ -4,12 +4,16 @@ Full code review performed 2026-04-05 across all 467 source files. ## Critical Bugs -- [ ] **Thread safety: Engine.run() race condition** — `Engine.java:152`. Render thread drains TransactionBuffer while logic thread writes concurrently. TransactionBuffer has no synchronization. Use TransactionBus (which has per-subscriber locks) or add synchronization. -- [ ] **Profiler.lastFrame() returns currentFrame** — `Profiler.java:29`. Method named `lastFrame()` returns `currentFrame` (in-progress). `previousFrame()` returns `lastFrame`. Either rename or fix return values. -- [ ] **GPU buffer leak: per-entity UBOs never cleaned up** — `UniformManager.java:79`. Creates objectUBOs ("obj_N") and materialUBOs ("mat_N") per entity. Never removed on EntityRemoved. Leaks GPU buffers for every entity that ever existed. -- [ ] **Stale shader binding after entity reuse** — `ShaderManager.java:168`. entityShaders keyed by handle index only, no generation check. After entity destroy+recreate at same index, old shader persists. -- [ ] **Engine.shutdown() doesn't shut down AssetManager** — `Engine.java:187`. FileWatcher thread keeps running. Add `assets.shutdown()` to teardown. -- [ ] **Renderer.close() doesn't clean up MeshManager** — `Renderer.java:314`. MeshManager holds GPU resources (VBOs, IBOs, VAOs) via meshDataCache that are never released on shutdown. +- [x] **Thread safety: Engine.run() race condition** — `Engine.java:152`. Render thread drains TransactionBuffer while logic thread writes concurrently. TransactionBuffer has no synchronization. Use TransactionBus (which has per-subscriber locks) or add synchronization. +- [x] **Profiler.lastFrame() returns currentFrame** — `Profiler.java:29`. Renamed to `currentFrame()`; `lastFrame()` now correctly returns the previous completed frame. Tests updated accordingly. +- [x] **GPU buffer leak: per-entity UBOs never cleaned up** — `UniformManager.java`. Per-entity UBO maps now keyed by `Handle` (index + generation) instead of string. `removeEntity()` destroys the buffers when an entity is removed. `Renderer.renderFrame()` calls it for each `EntityRemoved` transaction. +- [x] **Stale shader binding after entity reuse** — `ShaderManager.java`. `removeEntityShader()` added; called by `Renderer.renderFrame()` on `EntityRemoved`. +- [x] **Engine.shutdown() doesn't shut down AssetManager** — `Engine.java`. Added `assets.shutdown()` to `shutdown()`. +- [x] **Renderer.close() doesn't clean up MeshManager** — `Renderer.java`. Added `meshManager.close()` to `close()`. Added `MeshManager.close()` that destroys all GPU buffers and vertex inputs. +- [x] **MeshRenderer leaks entity maps on EntityRemoved** — `MeshRenderer.java`. `meshDataAssignments`, `meshAssignments`, and `materialAssignments` were not cleared on `EntityRemoved`. +- [x] **Vec2 alignment missing from material UBO write loop** — `UniformManager.java`. Size calculation aligned Vec2 to 8 bytes, but the write offset loop did not. Produced a layout mismatch (GPU reads wrong bytes for any material with Vec2 properties after an unaligned predecessor). Fixed by adding `offset = align(offset, 8)` before writing Vec2. +- [x] **Remaining hardcoded `"Object"` string in `bindGlobals()`** — `UniformManager.java`. The `bindGlobals()` method still used the raw literal `"Object"` instead of `GlobalParamNames.OBJECT`. Now consistent with the rest of the class. +- [x] **Material UBO silently wrong-sized on property change** — `UniformManager.uploadAndBindMaterial()`. Added `materialUboSizes` tracking map. When the required UBO size exceeds the allocated size (e.g., after a material gains new scalar properties), the old buffer is destroyed and a correctly-sized buffer is reallocated. ## Hardcoded Values (should be configurable/dynamic) @@ -19,8 +23,8 @@ Full code review performed 2026-04-05 across all 467 source files. - [ ] **Texture/sampler array sizes: magic numbers** — GL: boundTextures[32], boundSamplers[32]. Vk: currentTextures[8], pendingUboBuffers[16], pendingSsboBuffers[8]. Should be queried from DeviceCapability or configurable. - [ ] **MAX_FRAMES_IN_FLIGHT=2 hardcoded** — `VkRenderDevice.java:47`. Should be configurable through VulkanConfig. - [ ] **MAX_SETS_PER_FRAME=256 hardcoded** — `VkDescriptorManager.java:17`. Could be insufficient for complex scenes. Should auto-grow or be configurable. -- [ ] **Global param bindings hardcoded** — `Renderer.java:79-81`. "Engine"=0, "Camera"=1, "Object"=2 are string literals scattered throughout. Define as constants, centralize binding assignment. -- [ ] **Blend function hardcoded to SRC_ALPHA/ONE_MINUS_SRC_ALPHA** — `GlRenderDevice.java:623`. BlendMode exists but only supports NONE vs one hardcoded alpha blend. Need configurable src/dst factors, blend equation. +- [x] **Global param bindings hardcoded** — `Renderer.java:79-81`. Extracted to `GlobalParamNames` constants class (`ENGINE`, `CAMERA`, `OBJECT`). Used in `Renderer` and `UniformManager`. +- [x] **Blend function hardcoded to SRC_ALPHA/ONE_MINUS_SRC_ALPHA** — Added `BlendFactor` and `BlendEquation` interfaces; extended `BlendMode` with separate color/alpha factor and equation accessors; added factory `BlendMode.of(...)` for custom blending; all three backends (GL/Vk/WebGPU) now derive blend factors from the `BlendMode` object using `glBlendFuncSeparate`/`glBlendEquationSeparate`. Fixed incorrect Vulkan blend-factor constants (DST_COLOR was 8→4, DST_ALPHA was 10→8). Fixed legacy `SetBlending` command to use `BlendMode.ALPHA` instead of hardcoded values. Added `RenderState.BLEND_MODES` (`BlendMode[]`) for per-attachment MRT blending: GL uses `glBlendFuncSeparatei`/`glBlendEquationSeparatei`/`glEnablei`/`glDisablei`; Vulkan creates one `VkPipelineColorBlendAttachmentState` per attachment; WebGPU silently falls back to single-target blending (current binding limitation). - [ ] **All shaders forced to STANDARD_FORMAT vertex layout** — `ShaderManager.java:255`. Uses PrimitiveMeshes.STANDARD_FORMAT for ALL pipelines. Custom vertex formats (tangents, colors, bone weights) won't work. - [ ] **Shader entry points hardcoded ("vertexMain"/"fragmentMain")** — `ShaderManager.java:227`. Should be configurable per shader for custom entry points. - [ ] **Camera defaults (near=0.1, far=1000) not in config** — `Camera.java:15`. Should be configurable through EngineConfig or named constants. @@ -30,7 +34,7 @@ Full code review performed 2026-04-05 across all 467 source files. - [ ] **EngineConfig missing common options** — Missing: FPS cap, VSync toggle, MSAA sample count, anisotropic filtering, gamma/sRGB mode, fullscreen mode, monitor selection, cursor visibility, debug overlay toggle. - [ ] **GraphicsConfig missing graphics settings** — Only has headless and validation. Missing: MSAA, VSync mode, sRGB framebuffer, aniso level, GPU selection, shader cache directory, max texture size override. -- [ ] **WindowDescriptor too minimal** — Only title/width/height. Missing: resizable, fullscreen, decorated, transparent, always-on-top, min/max size, initial position, high-DPI flag, cursor mode. +- [x] **WindowDescriptor too minimal** — Created `WindowConfig` record in `graphics/api` with `title`, `width`, `height`, `resizable`, `decorated`, `vsync`, `fullscreen`, `alwaysOnTop`. `EngineConfig` now holds a `WindowConfig` (backward-compat `windowTitle()`/`windowSize()` accessors retained); builder gets `windowResizable()`, `windowDecorated()`, `windowVsync()`, `windowFullscreen()`, `windowAlwaysOnTop()`. `BaseApplication` applies all properties after window creation. Added `WindowProperty.ALWAYS_ON_TOP` (GLFW: `GLFW_FLOATING`). Still missing: transparent framebuffer, min/max size, initial position, high-DPI flag, cursor mode (require toolkit hints or additional GLFW calls). ## Designed but Not Implemented (from NOTES.md) @@ -65,12 +69,18 @@ Full code review performed 2026-04-05 across all 467 source files. - [ ] **No lighting system** — LightData/LightType exist in core but Renderer has zero light handling. No light buffer upload, no light culling. MeshRenderer ignores LightData components. - [ ] **No shadow mapping** — Extensively designed in NOTES.md. No shadow pass, no shadow maps, no light-space matrices. LightData.CASTS_SHADOWS exists but nothing reads it. -- [ ] **RenderStats never populated** — `RenderStats.java`. Tracks draws, vertices, binds but nothing calls recordDrawCall() etc. All counters always zero. +- [x] **RenderStats never populated** — `RenderStats.java`. `Renderer.renderFrame()` now calls `recordDrawCall()`, `recordPipelineBind()`, `recordBufferBind()`, `recordTextureBind()`. Stats are reset at the top of each `renderFrame()` call. `Engine.renderStats()` now delegates to `renderer.renderStats()` (stats are owned by Renderer). + +## Code Quality / API + +- [x] **BaseApplication Javadoc example uses deprecated API** — Updated to show the new `.graphics(new OpenGlConfig(...))` API. +- [x] **`BaseApplication` sets viewport twice per frame** — Removed unconditional per-frame `setViewport` call. Viewport is now set once before the loop and updated only on `WindowEvent.Resized`. +- [x] **`DrawCommand.materialData` raw unchecked generic** — Changed to `PropertyMap` for type safety. ## Architectural Cleanup - [ ] **Deprecated APIs to remove** — GraphicsBackendFactory, GraphicsConfigLegacy, EngineConfig.graphicsBackend field, GlfwWindowToolkit in graphics:opengl, deprecated SetDepthTest/SetBlending/SetCullFace/SetWireframe commands. New APIs (GraphicsConfig, SetRenderState) are in place. -- [ ] **ResourceCleaner.register() package-private** — Can't be used outside core.resource. Make public or provide public API for Cleaner safety net. +- [x] **ResourceCleaner.register() package-private** — Made `public` so GPU resource owners outside `core.resource` can register cleanup actions. - [ ] **NativeResource not used by GPU resources** — Interface exists but no GPU resource implements it. No Cleaner safety net for dropped handles. - [ ] **GpuResourceManager deferred deletion delay doesn't match frames-in-flight** — `GpuResourceManager.java:175`. Double-buffer swap means N+2 deletion. If Vulkan uses 3 frames-in-flight, resources freed while still in use. Tie to actual fence/frame count. @@ -83,3 +93,14 @@ Full code review performed 2026-04-05 across all 467 source files. - [ ] **Async buffer mapping for TeaVM WebGPU** — Browser can't do synchronous mapping. Needs promise wrapper or skip readback on web. - [ ] **Slang generic specialization parsing robustness** — `SlangCompilerNative.java:245-276`. Current regex works. Revisit when it breaks. - [ ] **WGSL binding extraction regex** — `WgpuRenderDevice.java:623-625`. Works for current shaders. Revisit on Slang output changes. + +## Debug UI (new, from 2026-04-05 rebase review) + +- [x] **`DebugUiOverlay.close()` leaked pipeline handle** — `DebugUiOverlay.java`. The pipeline was created via `ShaderManager.compileSlangSource()` which does NOT cache the pipeline in `ShaderManager.shaderCache`. The original `close()` comment ("Pipeline is owned by ShaderManager") was incorrect; the pipeline is owned by `DebugUiOverlay`. Fixed by adding `device.destroyPipeline(pipeline)` to `close()`. +- [ ] **`DebugUiOverlay` always allocated even in headless mode** — `Engine.java`. `Engine` constructor always creates `NkBuiltinFont` (builds 128×64 atlas in memory) and calls `DebugUiOverlay.init()`. The shader compilation failure is caught and `initialized` stays false, but font atlas allocation is wasted work in tests. Consider a lazy `initUi()` method or guard on `config.headless()`. +- [ ] **`DebugUiOverlay.render()` scissor clamping only in WebGPU** — `WgpuRenderDevice.java` clamps scissor to RT size, but `GlRenderDevice` and `VkRenderDevice` do not. Negative or out-of-bounds scissor rects cause driver errors (GL: GL_INVALID_VALUE). The flipScissorY conversion can produce negative Y for small scissors near the top; RenderDevice scissor API should clamp to [0, viewport] on all backends. +- [ ] **`NkContext` no scroll velocity / smooth scroll** — `NkContext.java`. Mouse scroll immediately jumps; no momentum or smooth scroll. Fine for a debug overlay but noticeable for any scrollable list. +- [ ] **`NkContext.begin()` creates windows on first call, never removes them** — `NkContext.java`. `windows` map grows unboundedly if dynamic window names are used. `NkWindow.closed = true` hides but doesn't remove. Consider evicting windows not opened in N frames. +- [ ] **`NkDrawList` index buffer uses `short` (max 65535 vertices)** — `NkDrawList.java`. 16-bit indices limit a single draw list to 65535 unique vertices. Fine for small debug overlays; will silently corrupt for complex UI. +- [ ] **`debug_ui.slang` `[[vk::binding]]` applies to all targets** — `debug_ui.slang:33`. Slang propagates `[[vk::binding(N)]]` to WGSL output, forcing binding index N even in WebGPU where no such offset exists. For WebGPU the texture binding index should be 0. Currently `textureBindingOffset=0` for WebGPU so `N=0`; if that changes this will silently break WebGPU UI rendering. The `ShaderManager.prependParamBlocks` already guards SPIRV-only annotation for material textures — `debug_ui.slang` should do the same or be generated rather than hardcoded. + diff --git a/core/src/main/java/dev/engine/core/profiler/Profiler.java b/core/src/main/java/dev/engine/core/profiler/Profiler.java index 2aed624f..6ba59947 100644 --- a/core/src/main/java/dev/engine/core/profiler/Profiler.java +++ b/core/src/main/java/dev/engine/core/profiler/Profiler.java @@ -26,11 +26,11 @@ public void newFrame() { } public Map lastFrame() { - return currentFrame; + return lastFrame; } - public Map previousFrame() { - return lastFrame; + public Map currentFrame() { + return currentFrame; } private void endScope(ScopeEntry entry) { diff --git a/core/src/main/java/dev/engine/core/resource/ResourceCleaner.java b/core/src/main/java/dev/engine/core/resource/ResourceCleaner.java index b70a726f..13014cb2 100644 --- a/core/src/main/java/dev/engine/core/resource/ResourceCleaner.java +++ b/core/src/main/java/dev/engine/core/resource/ResourceCleaner.java @@ -8,7 +8,7 @@ public final class ResourceCleaner { private ResourceCleaner() {} - static Cleaner.Cleanable register(Object resource, Runnable cleanupAction) { + public static Cleaner.Cleanable register(Object resource, Runnable cleanupAction) { return CLEANER.register(resource, cleanupAction); } } diff --git a/core/src/test/java/dev/engine/core/profiler/ProfilerTest.java b/core/src/test/java/dev/engine/core/profiler/ProfilerTest.java index a748aa22..a22599e9 100644 --- a/core/src/test/java/dev/engine/core/profiler/ProfilerTest.java +++ b/core/src/test/java/dev/engine/core/profiler/ProfilerTest.java @@ -19,7 +19,7 @@ class BasicScopes { try (var scope = profiler.scope("render")) { spinWait(1_000_000); // ~1ms } - var results = profiler.lastFrame(); + var results = profiler.currentFrame(); assertNotNull(results); var render = results.get("render"); assertNotNull(render); @@ -35,7 +35,7 @@ class BasicScopes { spinWait(500_000); } } - var results = profiler.lastFrame(); + var results = profiler.currentFrame(); var frame = results.get("frame"); assertNotNull(frame); assertEquals(2, frame.children().size()); @@ -49,7 +49,7 @@ class BasicScopes { spinWait(500_000); } } - var results = profiler.lastFrame(); + var results = profiler.currentFrame(); var parent = results.get("parent"); var child = parent.children().get("child"); assertTrue(parent.cpuNanos() >= child.cpuNanos()); @@ -62,7 +62,7 @@ class FrameReset { try (var s = profiler.scope("old")) { spinWait(100_000); } profiler.newFrame(); try (var s = profiler.scope("new")) { spinWait(100_000); } - var results = profiler.lastFrame(); + var results = profiler.currentFrame(); assertNull(results.get("old")); assertNotNull(results.get("new")); } diff --git a/docs/guides/code-review-2026-04-05.md b/docs/guides/code-review-2026-04-05.md new file mode 100644 index 00000000..526a2099 --- /dev/null +++ b/docs/guides/code-review-2026-04-05.md @@ -0,0 +1,141 @@ +# Code Review Findings (2026-04-05) + +This document records the findings, decisions, and fixes from the full code review +performed in April 2026. Items still open are tracked in [TODO.md](../../TODO.md). + +--- + +## Bugs Fixed in This Review + +### Profiler: `lastFrame()` / `currentFrame()` naming confusion + +**Symptom:** `Profiler.lastFrame()` was documented as "the last completed frame" but +actually returned the *in-progress* current frame. `previousFrame()` returned the +truly last completed frame. + +**Fix:** Renamed the two methods to match their actual semantics: + +| New name | Returns | Notes | +|---|---|---| +| `currentFrame()` | In-progress frame map | Write during the frame | +| `lastFrame()` | Most recently completed frame | Read by debug overlays / profiling | + +Tests updated to use `currentFrame()` where they were reading in-frame data. + +--- + +### GPU buffer leak: per-entity UBOs (`obj_N`, `mat_N`) + +**Symptom:** `UniformManager` allocated a UBO per entity for the object transform and +for the material scalars. The maps were keyed by `"obj_" + entity.index()` (an `int`), +which ignores the handle generation. Buffers were never freed when entities were +destroyed. + +**Consequences:** +1. Unbounded GPU memory growth for every entity that was ever created and destroyed. +2. After an entity slot was reused (same index, new generation) the new entity would + silently reuse the old entity's GPU buffer without reallocating, leading to stale + transform/material data for one frame. + +**Fix:** +- Maps now keyed by `Handle` (record with `index` + `generation`) so old entries are + never reused by new entities at the same slot. +- Added `UniformManager.removeEntity(Handle entity)` which destroys and removes the + entity's object UBO and material UBO. +- `Renderer.renderFrame()` calls `removeEntity()` for every `EntityRemoved` transaction + *before* passing transactions to `MeshRenderer`. + +--- + +### Stale shader binding after entity slot reuse + +**Symptom:** `ShaderManager.entityShaders` mapped `entity.index()` → `CompiledShader`. +After an entity was destroyed and its handle slot recycled, the new entity at the same +index inherited the old entity's compiled shader silently. + +**Fix:** Added `ShaderManager.removeEntityShader(Handle entity)` (clears by index). +`Renderer.renderFrame()` calls it for every `EntityRemoved` transaction. + +--- + +### `Engine.shutdown()` did not stop the `AssetManager` + +**Symptom:** If hot-reload was enabled (`AssetManager.enableHotReload()`), the +`FileWatcher` background thread kept running after `Engine.shutdown()`. + +**Fix:** Added `assets.shutdown()` call in `Engine.shutdown()`. + +--- + +### `Renderer.close()` did not clean up `MeshManager` + +**Symptom:** `MeshManager` holds GPU vertex buffers, index buffers, and vertex-input +descriptors via `meshRegistry` (handle-based meshes) and `meshDataCache` +(data-keyed meshes). Neither was freed at shutdown. + +**Fix:** Added `MeshManager.close()` which destroys all buffers and vertex inputs in +both maps. `Renderer.close()` now calls `meshManager.close()` first. + +--- + +### `MeshRenderer` leaked entity maps on `EntityRemoved` + +**Symptom:** `MeshRenderer.processTransaction` for `EntityRemoved` removed the entity +from `transforms`, `renderables`, `materials`, and `materialData`, but left behind +entries in `meshDataAssignments`, `meshAssignments`, and `materialAssignments`. + +**Fix:** All seven maps are now cleared on `EntityRemoved`. + +--- + +### `RenderStats` was never populated + +**Symptom:** `RenderStats` tracks draw calls, vertex counts, pipeline binds, texture +binds, and buffer binds. No code ever called any `record*` method. All counters were +permanently zero. + +**Fix:** `Renderer.renderFrame()` now calls: +- `renderStats.reset()` at the top of each frame +- `renderStats.recordPipelineBind()` on each `bindPipeline` +- `renderStats.recordDrawCall(vertices, indices)` on each `draw` / `drawIndexed` +- `renderStats.recordBufferBind()` on material UBO bind +- `renderStats.recordTextureBind()` on material texture bind + +`Renderer.renderStats()` exposes the stats object. `Engine.renderStats()` now +delegates to `renderer.renderStats()` (previously Engine owned a separate, unused +instance). + +--- + +### `ResourceCleaner.register()` was package-private + +**Symptom:** The `java.lang.ref.Cleaner` safety net for native resources could only +be used inside the `dev.engine.core.resource` package. + +**Fix:** Made the method `public`. + +--- + +### Global param name string literals scattered through code + +**Symptom:** Strings `"Engine"`, `"Camera"`, `"Object"` appeared as raw literals in +`Renderer`, `UniformManager`, `ShaderManager`, and shader generation code. A typo +would silently break param binding with no compile-time error. + +**Fix:** Added `GlobalParamNames` constants class in `dev.engine.graphics.shader` with +`ENGINE`, `CAMERA`, and `OBJECT` constants. `Renderer` and `UniformManager` updated +to use them. + +--- + +## Items Still Open (see TODO.md for full list) + +- Thread safety: `Engine.run()` race on `TransactionBuffer` +- Primitive topology hardcoded to TRIANGLES +- No frustum culling +- No lighting/shadow system +- RenderGraph/PostProcessChain/UploadStrategy implemented but unused +- Asset eviction / reference counting / async loading +- Disk shader cache +- Shader hot-reload not wired +- Error/fallback shader (currently returns null → entity disappears) diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderer/DrawCommand.java b/graphics/api/src/main/java/dev/engine/graphics/renderer/DrawCommand.java index 029e5825..da647a29 100644 --- a/graphics/api/src/main/java/dev/engine/graphics/renderer/DrawCommand.java +++ b/graphics/api/src/main/java/dev/engine/graphics/renderer/DrawCommand.java @@ -1,7 +1,8 @@ package dev.engine.graphics.renderer; import dev.engine.core.handle.Handle; +import dev.engine.core.material.MaterialData; import dev.engine.core.math.Mat4; import dev.engine.core.property.PropertyMap; -public record DrawCommand(Handle entity, Renderable renderable, Mat4 transform, PropertyMap materialData) {} +public record DrawCommand(Handle entity, Renderable renderable, Mat4 transform, PropertyMap materialData) {} diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderer/MeshRenderer.java b/graphics/api/src/main/java/dev/engine/graphics/renderer/MeshRenderer.java index 038e011c..417645fa 100644 --- a/graphics/api/src/main/java/dev/engine/graphics/renderer/MeshRenderer.java +++ b/graphics/api/src/main/java/dev/engine/graphics/renderer/MeshRenderer.java @@ -44,6 +44,9 @@ public void processTransaction(Transaction txn) { renderables.remove(removed.entity()); materials.remove(removed.entity()); materialData.remove(removed.entity()); + meshDataAssignments.remove(removed.entity()); + meshAssignments.remove(removed.entity()); + materialAssignments.remove(removed.entity()); } case Transaction.ComponentChanged cc -> processComponentChanged(cc); // Legacy transaction types — kept for compatibility diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendEquation.java b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendEquation.java new file mode 100644 index 00000000..3f71379c --- /dev/null +++ b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendEquation.java @@ -0,0 +1,16 @@ +package dev.engine.graphics.renderstate; + +/** + * Blend equation defining how source and destination values are combined. + * + *

result = src * srcFactor OP dst * dstFactor + */ +public interface BlendEquation { + String name(); + + BlendEquation ADD = () -> "ADD"; + BlendEquation SUBTRACT = () -> "SUBTRACT"; + BlendEquation REVERSE_SUBTRACT = () -> "REVERSE_SUBTRACT"; + BlendEquation MIN = () -> "MIN"; + BlendEquation MAX = () -> "MAX"; +} diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendFactor.java b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendFactor.java new file mode 100644 index 00000000..fca87755 --- /dev/null +++ b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendFactor.java @@ -0,0 +1,22 @@ +package dev.engine.graphics.renderstate; + +/** + * Blend factor for the blend equation: result = src * srcFactor OP dst * dstFactor. + * + *

Use with {@link BlendMode#of} to create custom blend configurations. + * Backends map these to their own factor constants. + */ +public interface BlendFactor { + String name(); + + BlendFactor ZERO = () -> "ZERO"; + BlendFactor ONE = () -> "ONE"; + BlendFactor SRC_COLOR = () -> "SRC_COLOR"; + BlendFactor ONE_MINUS_SRC_COLOR = () -> "ONE_MINUS_SRC_COLOR"; + BlendFactor DST_COLOR = () -> "DST_COLOR"; + BlendFactor ONE_MINUS_DST_COLOR = () -> "ONE_MINUS_DST_COLOR"; + BlendFactor SRC_ALPHA = () -> "SRC_ALPHA"; + BlendFactor ONE_MINUS_SRC_ALPHA = () -> "ONE_MINUS_SRC_ALPHA"; + BlendFactor DST_ALPHA = () -> "DST_ALPHA"; + BlendFactor ONE_MINUS_DST_ALPHA = () -> "ONE_MINUS_DST_ALPHA"; +} diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendMode.java b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendMode.java index fd66d954..afad7a60 100644 --- a/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendMode.java +++ b/graphics/api/src/main/java/dev/engine/graphics/renderstate/BlendMode.java @@ -1,11 +1,114 @@ package dev.engine.graphics.renderstate; +/** + * Blending configuration specifying how source and destination colors are combined. + * + *

Use the predefined constants ({@link #NONE}, {@link #ALPHA}, {@link #ADDITIVE}, + * {@link #MULTIPLY}, {@link #PREMULTIPLIED}) for common cases, or create a custom + * blend mode with {@link #of}. + * + *

{@code
+ * // Predefined:
+ * renderState.set(RenderState.BLEND_MODE, BlendMode.ALPHA);
+ *
+ * // Custom (e.g., screen blend):
+ * var screen = BlendMode.of("SCREEN",
+ *         BlendFactor.ONE, BlendFactor.ONE_MINUS_SRC_COLOR,
+ *         BlendFactor.ONE, BlendFactor.ONE_MINUS_SRC_ALPHA);
+ * renderState.set(RenderState.BLEND_MODE, screen);
+ * }
+ */ public interface BlendMode { String name(); + boolean enabled(); + BlendFactor srcColorFactor(); + BlendFactor dstColorFactor(); + BlendFactor srcAlphaFactor(); + BlendFactor dstAlphaFactor(); + BlendEquation colorEquation(); + BlendEquation alphaEquation(); - BlendMode NONE = () -> "NONE"; - BlendMode ALPHA = () -> "ALPHA"; - BlendMode ADDITIVE = () -> "ADDITIVE"; - BlendMode MULTIPLY = () -> "MULTIPLY"; - BlendMode PREMULTIPLIED = () -> "PREMULTIPLIED"; + /** Creates a custom blend mode with the same factors for color and alpha. ADD equation. */ + static BlendMode of(String name, BlendFactor src, BlendFactor dst) { + return of(name, src, dst, src, dst); + } + + /** Creates a custom blend mode with separate color and alpha factors. ADD equation. */ + static BlendMode of(String name, BlendFactor srcColor, BlendFactor dstColor, + BlendFactor srcAlpha, BlendFactor dstAlpha) { + return of(name, srcColor, dstColor, srcAlpha, dstAlpha, + BlendEquation.ADD, BlendEquation.ADD); + } + + /** Creates a fully customized blend mode. */ + static BlendMode of(String name, BlendFactor srcColor, BlendFactor dstColor, + BlendFactor srcAlpha, BlendFactor dstAlpha, + BlendEquation colorEq, BlendEquation alphaEq) { + return new BlendMode() { + public String name() { return name; } + public boolean enabled() { return true; } + public BlendFactor srcColorFactor() { return srcColor; } + public BlendFactor dstColorFactor() { return dstColor; } + public BlendFactor srcAlphaFactor() { return srcAlpha; } + public BlendFactor dstAlphaFactor() { return dstAlpha; } + public BlendEquation colorEquation() { return colorEq; } + public BlendEquation alphaEquation() { return alphaEq; } + }; + } + + BlendMode NONE = new BlendMode() { + public String name() { return "NONE"; } + public boolean enabled() { return false; } + public BlendFactor srcColorFactor() { return BlendFactor.ONE; } + public BlendFactor dstColorFactor() { return BlendFactor.ZERO; } + public BlendFactor srcAlphaFactor() { return BlendFactor.ONE; } + public BlendFactor dstAlphaFactor() { return BlendFactor.ZERO; } + public BlendEquation colorEquation() { return BlendEquation.ADD; } + public BlendEquation alphaEquation() { return BlendEquation.ADD; } + }; + + BlendMode ALPHA = new BlendMode() { + public String name() { return "ALPHA"; } + public boolean enabled() { return true; } + public BlendFactor srcColorFactor() { return BlendFactor.SRC_ALPHA; } + public BlendFactor dstColorFactor() { return BlendFactor.ONE_MINUS_SRC_ALPHA; } + public BlendFactor srcAlphaFactor() { return BlendFactor.ONE; } + public BlendFactor dstAlphaFactor() { return BlendFactor.ONE_MINUS_SRC_ALPHA; } + public BlendEquation colorEquation() { return BlendEquation.ADD; } + public BlendEquation alphaEquation() { return BlendEquation.ADD; } + }; + + BlendMode ADDITIVE = new BlendMode() { + public String name() { return "ADDITIVE"; } + public boolean enabled() { return true; } + public BlendFactor srcColorFactor() { return BlendFactor.SRC_ALPHA; } + public BlendFactor dstColorFactor() { return BlendFactor.ONE; } + public BlendFactor srcAlphaFactor() { return BlendFactor.ONE; } + public BlendFactor dstAlphaFactor() { return BlendFactor.ONE; } + public BlendEquation colorEquation() { return BlendEquation.ADD; } + public BlendEquation alphaEquation() { return BlendEquation.ADD; } + }; + + BlendMode MULTIPLY = new BlendMode() { + public String name() { return "MULTIPLY"; } + public boolean enabled() { return true; } + public BlendFactor srcColorFactor() { return BlendFactor.DST_COLOR; } + public BlendFactor dstColorFactor() { return BlendFactor.ZERO; } + public BlendFactor srcAlphaFactor() { return BlendFactor.DST_ALPHA; } + public BlendFactor dstAlphaFactor() { return BlendFactor.ZERO; } + public BlendEquation colorEquation() { return BlendEquation.ADD; } + public BlendEquation alphaEquation() { return BlendEquation.ADD; } + }; + + BlendMode PREMULTIPLIED = new BlendMode() { + public String name() { return "PREMULTIPLIED"; } + public boolean enabled() { return true; } + public BlendFactor srcColorFactor() { return BlendFactor.ONE; } + public BlendFactor dstColorFactor() { return BlendFactor.ONE_MINUS_SRC_ALPHA; } + public BlendFactor srcAlphaFactor() { return BlendFactor.ONE; } + public BlendFactor dstAlphaFactor() { return BlendFactor.ONE_MINUS_SRC_ALPHA; } + public BlendEquation colorEquation() { return BlendEquation.ADD; } + public BlendEquation alphaEquation() { return BlendEquation.ADD; } + }; } + diff --git a/graphics/api/src/main/java/dev/engine/graphics/renderstate/RenderState.java b/graphics/api/src/main/java/dev/engine/graphics/renderstate/RenderState.java index 6b9e4716..adec030a 100644 --- a/graphics/api/src/main/java/dev/engine/graphics/renderstate/RenderState.java +++ b/graphics/api/src/main/java/dev/engine/graphics/renderstate/RenderState.java @@ -7,7 +7,22 @@ public interface RenderState { PropertyKey DEPTH_TEST = PropertyKey.of("depthTest", Boolean.class); PropertyKey DEPTH_WRITE = PropertyKey.of("depthWrite", Boolean.class); PropertyKey DEPTH_FUNC = PropertyKey.of("depthFunc", CompareFunc.class); - PropertyKey BLEND_MODE = PropertyKey.of("blendMode", BlendMode.class); + PropertyKey BLEND_MODE = PropertyKey.of("blendMode", BlendMode.class); + /** + * Per-attachment blend modes for Multiple Render Target (MRT) rendering. + * + *

When set, index {@code i} of the array is used for color attachment {@code i}. + * If the array is shorter than the number of active attachments, the last entry is + * repeated for the remaining attachments. {@link #BLEND_MODE} is used as the + * fallback when this key is absent. + * + *

OpenGL 4.0+ ({@code glBlendFuncSeparatei} / {@code glBlendEquationSeparatei}) + * and Vulkan ({@code VkPipelineColorBlendAttachmentState} per attachment) both + * support independent attachment blending. WebGPU exposes the same capability + * in the pipeline descriptor but the current binding only surfaces a single + * color target — per-attachment overrides are silently ignored on WebGPU. + */ + PropertyKey BLEND_MODES = PropertyKey.of("blendModes", BlendMode[].class); PropertyKey CULL_MODE = PropertyKey.of("cullMode", CullMode.class); PropertyKey FRONT_FACE = PropertyKey.of("frontFace", FrontFace.class); PropertyKey WIREFRAME = PropertyKey.of("wireframe", Boolean.class); diff --git a/graphics/api/src/main/java/dev/engine/graphics/shader/GlobalParamNames.java b/graphics/api/src/main/java/dev/engine/graphics/shader/GlobalParamNames.java new file mode 100644 index 00000000..1ddb4632 --- /dev/null +++ b/graphics/api/src/main/java/dev/engine/graphics/shader/GlobalParamNames.java @@ -0,0 +1,29 @@ +package dev.engine.graphics.shader; + +/** + * Well-known names for the built-in global shader parameter blocks. + * + *

Use these constants instead of raw string literals when referencing the + * built-in engine, camera, and object param blocks to avoid typos and make + * refactoring easier. + * + *

The corresponding binding indices are the defaults registered by the engine: + *

    + *
  • {@link #ENGINE} → binding 0
  • + *
  • {@link #CAMERA} → binding 1
  • + *
  • {@link #OBJECT} → binding 2
  • + *
+ */ +public final class GlobalParamNames { + + /** Name of the per-frame engine parameter block (time, delta, resolution, frame count). */ + public static final String ENGINE = "Engine"; + + /** Name of the per-frame camera parameter block (view/projection matrices, position, clip planes). */ + public static final String CAMERA = "Camera"; + + /** Name of the per-draw object parameter block (model matrix). */ + public static final String OBJECT = "Object"; + + private GlobalParamNames() {} +} diff --git a/graphics/api/src/main/java/dev/engine/graphics/window/WindowConfig.java b/graphics/api/src/main/java/dev/engine/graphics/window/WindowConfig.java new file mode 100644 index 00000000..3305ab99 --- /dev/null +++ b/graphics/api/src/main/java/dev/engine/graphics/window/WindowConfig.java @@ -0,0 +1,66 @@ +package dev.engine.graphics.window; + +/** + * Window configuration that is applied at startup. + * + *

Contains all window properties that should be set when the window is created. + * Properties are applied via {@link WindowHandle#set} after window creation. + * + *

{@code
+ * var config = EngineConfig.builder()
+ *     .windowTitle("My Game")
+ *     .windowSize(1280, 720)
+ *     .windowResizable(true)
+ *     .windowDecorated(true)
+ *     .windowVsync(true)
+ *     .build();
+ * }
+ */ +public record WindowConfig( + String title, + int width, + int height, + boolean resizable, + boolean decorated, + boolean vsync, + boolean fullscreen, + boolean alwaysOnTop +) { + public WindowConfig { + if (title == null) title = "Engine"; + if (width <= 0) throw new IllegalArgumentException("width must be > 0"); + if (height <= 0) throw new IllegalArgumentException("height must be > 0"); + } + + /** Creates a WindowDescriptor for the backend window creation call. */ + public WindowDescriptor toDescriptor() { + return new WindowDescriptor(title, width, height); + } + + public static Builder builder() { return new Builder(); } + + public static class Builder { + private String title = "Engine"; + private int width = 1280; + private int height = 720; + private boolean resizable = true; + private boolean decorated = true; + private boolean vsync = false; + private boolean fullscreen = false; + private boolean alwaysOnTop = false; + + public Builder title(String title) { this.title = title; return this; } + public Builder width(int width) { this.width = width; return this; } + public Builder height(int height) { this.height = height; return this; } + public Builder size(int width, int height) { this.width = width; this.height = height; return this; } + public Builder resizable(boolean v) { this.resizable = v; return this; } + public Builder decorated(boolean v) { this.decorated = v; return this; } + public Builder vsync(boolean v) { this.vsync = v; return this; } + public Builder fullscreen(boolean v) { this.fullscreen = v; return this; } + public Builder alwaysOnTop(boolean v) { this.alwaysOnTop = v; return this; } + + public WindowConfig build() { + return new WindowConfig(title, width, height, resizable, decorated, vsync, fullscreen, alwaysOnTop); + } + } +} diff --git a/graphics/api/src/main/java/dev/engine/graphics/window/WindowProperty.java b/graphics/api/src/main/java/dev/engine/graphics/window/WindowProperty.java index f63d418a..f6872deb 100644 --- a/graphics/api/src/main/java/dev/engine/graphics/window/WindowProperty.java +++ b/graphics/api/src/main/java/dev/engine/graphics/window/WindowProperty.java @@ -10,4 +10,5 @@ public interface WindowProperty { PropertyKey DECORATED = PropertyKey.of("window.decorated", Boolean.class); PropertyKey VISIBLE = PropertyKey.of("window.visible", Boolean.class); PropertyKey SWAP_INTERVAL = PropertyKey.of("window.swapInterval", Integer.class); + PropertyKey ALWAYS_ON_TOP = PropertyKey.of("window.alwaysOnTop", Boolean.class); } diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/DebugUiOverlay.java b/graphics/common/src/main/java/dev/engine/graphics/common/DebugUiOverlay.java index b1983875..b39d8ac8 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/DebugUiOverlay.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/DebugUiOverlay.java @@ -238,7 +238,7 @@ public void close() { if (fontTexture != null) device.destroyTexture(fontTexture); if (fontSampler != null) device.destroySampler(fontSampler); if (vertexInput != null) device.destroyVertexInput(vertexInput); - // Pipeline is owned by ShaderManager — don't destroy it here + if (pipeline != null) device.destroyPipeline(pipeline); initialized = false; } } diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/MeshManager.java b/graphics/common/src/main/java/dev/engine/graphics/common/MeshManager.java index 48e36092..9e18b5c9 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/MeshManager.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/MeshManager.java @@ -74,11 +74,26 @@ public MeshHandle resolve(Handle handle) { /** Polls for garbage-collected MeshData and destroys associated GPU resources. */ public void pollStale() { meshDataCache.pollStale(mesh -> { - gpu.destroyBuffer(mesh.vertexBuffer()); - if (mesh.indexBuffer() != null) gpu.destroyBuffer(mesh.indexBuffer()); + destroyMeshResources(mesh); }); } + /** Destroys all GPU resources held by this manager. Call on shutdown. */ + public void close() { + meshDataCache.clear(this::destroyMeshResources); + for (var mesh : meshRegistry.values()) { + destroyMeshResources(mesh); + } + meshRegistry.clear(); + } + + /** Destroys all GPU resources associated with a mesh handle. */ + private void destroyMeshResources(MeshHandle mesh) { + gpu.destroyBuffer(mesh.vertexBuffer()); + if (mesh.indexBuffer() != null) gpu.destroyBuffer(mesh.indexBuffer()); + gpu.destroyVertexInput(mesh.vertexInput()); + } + private MeshHandle uploadMeshData(MeshData data) { var buf = data.vertexData(); float[] vertices = new float[buf.remaining() / Float.BYTES]; diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/RenderStateManager.java b/graphics/common/src/main/java/dev/engine/graphics/common/RenderStateManager.java index d38d1dcc..68ad1a6f 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/RenderStateManager.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/RenderStateManager.java @@ -75,6 +75,7 @@ public PropertyMap resolve(MaterialData material) { public static boolean isRenderStateKey(PropertyKey key) { return key == RenderState.DEPTH_TEST || key == RenderState.DEPTH_WRITE || key == RenderState.DEPTH_FUNC || key == RenderState.BLEND_MODE + || key == RenderState.BLEND_MODES || key == RenderState.CULL_MODE || key == RenderState.FRONT_FACE || key == RenderState.WIREFRAME || key == RenderState.LINE_WIDTH || key == RenderState.SCISSOR_TEST diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/Renderer.java b/graphics/common/src/main/java/dev/engine/graphics/common/Renderer.java index dfc75518..0c34ebd6 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/Renderer.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/Renderer.java @@ -2,10 +2,12 @@ import dev.engine.core.handle.Handle; import dev.engine.core.math.Vec2; +import dev.engine.core.profiler.RenderStats; import dev.engine.core.property.PropertyKey; import dev.engine.graphics.renderstate.RenderState; import dev.engine.core.scene.MeshTag; import dev.engine.core.scene.camera.Camera; +import dev.engine.graphics.shader.GlobalParamNames; import dev.engine.graphics.shader.GlobalParamsRegistry; import dev.engine.graphics.shader.params.CameraParams; import dev.engine.graphics.shader.params.EngineParams; @@ -57,7 +59,8 @@ public class Renderer implements AutoCloseable { private float lastDeltaTime = 0f; private int frameCount = 0; - + // Per-frame render statistics (reset at the start of each renderFrame call) + private final RenderStats renderStats = new RenderStats(); // Viewport private Viewport viewport = Viewport.of(1, 1); // Placeholder until window sets actual size @@ -77,9 +80,9 @@ public Renderer(RenderDevice device, ShaderCompiler compiler) { this.samplerManager = new SamplerManager(gpu); var globalParams = new GlobalParamsRegistry(); - globalParams.register("Engine", EngineParams.class, 0); - globalParams.register("Camera", CameraParams.class, 1); - globalParams.register("Object", dev.engine.graphics.shader.params.ObjectParams.class, 2); + globalParams.register(GlobalParamNames.ENGINE, EngineParams.class, 0); + globalParams.register(GlobalParamNames.CAMERA, CameraParams.class, 1); + globalParams.register(GlobalParamNames.OBJECT, dev.engine.graphics.shader.params.ObjectParams.class, 2); this.uniformManager = new UniformManager(gpu, globalParams); this.shaderManager = new ShaderManager(device, globalParams, compiler); @@ -182,10 +185,28 @@ public void updateTime(float time, float deltaTime) { // --- Render --- public void renderFrame(List transactions) { + // Reset per-frame stats + renderStats.reset(); + // Poll stale GPU resources from weak caches meshManager.pollStale(); textureManager.pollStale(); + // Process entity removals first — release per-entity GPU resources before + // the render loop runs so removed entities don't contribute stale data. + // + // This deliberately iterates transactions twice (once here, once in + // meshRenderer.processTransactions). The separation is required for + // correctness: GPU resource cleanup (UniformManager, ShaderManager) must + // happen *before* MeshRenderer sees EntityRemoved so that the entity slot + // can be safely reused by a new entity added in the same batch. + for (var txn : transactions) { + if (txn instanceof dev.engine.core.transaction.Transaction.EntityRemoved removed) { + uniformManager.removeEntity(removed.entity()); + shaderManager.removeEntityShader(removed.entity()); + } + } + // Process transactions meshRenderer.processTransactions(transactions); @@ -229,12 +250,12 @@ public void renderFrame(List transactions) { device.submit(setup.finish()); // Update engine params - uniformManager.updateGlobalParams("Engine", new EngineParams(engineTime, lastDeltaTime, + uniformManager.updateGlobalParams(GlobalParamNames.ENGINE, new EngineParams(engineTime, lastDeltaTime, new Vec2(viewport.width(), viewport.height()), frameCount)); // Upload camera params if (activeCamera != null) { - uniformManager.updateGlobalParams("Camera", new CameraParams( + uniformManager.updateGlobalParams(GlobalParamNames.CAMERA, new CameraParams( activeCamera.viewProjectionMatrix(), activeCamera.viewMatrix(), activeCamera.projectionMatrix(), @@ -253,6 +274,7 @@ public void renderFrame(List transactions) { var draw = new CommandRecorder(); draw.bindPipeline(cmd.renderable().pipeline()); + renderStats.recordPipelineBind(); var mat = meshRenderer.getMaterialData(cmd.entity()); var renderState = renderStateManager.resolve(mat); @@ -264,10 +286,12 @@ public void renderFrame(List transactions) { int materialSlot = cmd.renderable().bindingFor("MaterialBuffer", uniformManager.globalParams().nextBinding()); uniformManager.uploadAndBindMaterial(mat, cmd.entity(), draw, materialSlot); + renderStats.recordBufferBind(); var compiled = shaderManager.getEntityShader(cmd.entity()); if (compiled != null) { textureManager.bindMaterialTextures(mat, compiled, samplerManager, draw); + renderStats.recordTextureBind(); } } @@ -275,8 +299,10 @@ public void renderFrame(List transactions) { if (cmd.renderable().indexBuffer() != null) { draw.bindIndexBuffer(cmd.renderable().indexBuffer()); draw.drawIndexed(cmd.renderable().indexCount(), 0); + renderStats.recordDrawCall(cmd.renderable().vertexCount(), cmd.renderable().indexCount()); } else { draw.draw(cmd.renderable().vertexCount(), 0); + renderStats.recordDrawCall(cmd.renderable().vertexCount(), 0); } device.submit(draw.finish()); } @@ -327,9 +353,11 @@ public String backendName() { public RenderTargetManager renderTargetManager() { return renderTargetManager; } public PipelineManager pipelineManager() { return pipelineManager; } public SamplerManager samplerManager() { return samplerManager; } + public RenderStats renderStats() { return renderStats; } @Override public void close() { + meshManager.close(); uniformManager.close(); textureManager.close(); renderTargetManager.close(); diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/ShaderManager.java b/graphics/common/src/main/java/dev/engine/graphics/common/ShaderManager.java index fe229d42..c8e69dd6 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/ShaderManager.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/ShaderManager.java @@ -203,6 +203,14 @@ public void invalidateAll() { entityShaders.clear(); } + /** + * Removes the cached shader binding for the given entity. + * Must be called when an entity is removed so that its slot can be reused safely. + */ + public void removeEntityShader(dev.engine.core.handle.Handle entity) { + entityShaders.remove(entity.index()); + } + // --- Internal --- private CompiledShader compileShader(String shaderName) { diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/UniformManager.java b/graphics/common/src/main/java/dev/engine/graphics/common/UniformManager.java index 182e870e..38a086d4 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/UniformManager.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/UniformManager.java @@ -8,6 +8,7 @@ import dev.engine.core.math.Vec2; import dev.engine.core.math.Vec3; import dev.engine.core.property.PropertyKey; +import dev.engine.graphics.shader.GlobalParamNames; import dev.engine.graphics.shader.GlobalParamsRegistry; import dev.engine.graphics.shader.params.ObjectParams; import dev.engine.graphics.BufferResource; @@ -28,8 +29,9 @@ public class UniformManager { private final GlobalParamsRegistry globalParams; private final Map> globalUbos = new HashMap<>(); private final Map globalLayouts = new HashMap<>(); - private final Map> materialUbos = new HashMap<>(); - private final Map> objectUbos = new HashMap<>(); + private final Map, Handle> materialUbos = new HashMap<>(); + private final Map, Integer> materialUboSizes = new HashMap<>(); + private final Map, Handle> objectUbos = new HashMap<>(); public UniformManager(GpuResourceManager gpu, GlobalParamsRegistry globalParams) { this.gpu = gpu; @@ -59,7 +61,7 @@ public void updateGlobalParams(String name, Object data) { public void uploadPerFrameGlobals() { for (var entry : globalParams.entries()) { if (entry.data() == null) continue; - if ("Object".equals(entry.name())) continue; + if (GlobalParamNames.OBJECT.equals(entry.name())) continue; var ubo = globalUbos.get(entry.name()); var layout = globalLayouts.get(entry.name()); if (ubo != null && layout != null) { @@ -72,11 +74,10 @@ public void uploadPerFrameGlobals() { /** Uploads per-object params and binds global UBOs to the draw command. */ public Handle uploadObjectParams(Handle entity, dev.engine.core.math.Mat4 transform) { - var objectLayout = globalLayouts.get("Object"); + var objectLayout = globalLayouts.get(GlobalParamNames.OBJECT); if (objectLayout == null) return null; - var objectKey = "obj_" + entity.index(); - var objectUbo = objectUbos.computeIfAbsent(objectKey, k -> + var objectUbo = objectUbos.computeIfAbsent(entity, k -> gpu.createBuffer( objectLayout.size(), BufferUsage.UNIFORM, AccessPattern.DYNAMIC)); var objectParams = new ObjectParams(transform); @@ -90,7 +91,7 @@ public Handle uploadObjectParams(Handle entity, dev.engine.co public void bindGlobals(CommandRecorder draw, dev.engine.graphics.renderer.Renderable r, Handle objectUbo) { for (var entry : globalParams.entries()) { Handle ubo; - if ("Object".equals(entry.name())) { + if (GlobalParamNames.OBJECT.equals(entry.name())) { ubo = objectUbo; } else { ubo = globalUbos.get(entry.name()); @@ -136,10 +137,19 @@ public void uploadAndBindMaterial(MaterialData matData, Handle entity, Comman totalSize = align(totalSize, maxAlign); final int uboSize = Math.max(totalSize, 16); - var uboKey = "mat_" + entity.index(); - var ubo = materialUbos.computeIfAbsent(uboKey, k -> - gpu.createBuffer( - uboSize, BufferUsage.UNIFORM, AccessPattern.DYNAMIC)); + var existingUbo = materialUbos.get(entity); + Integer existingSize = materialUboSizes.get(entity); + if (existingUbo == null || existingSize == null || existingSize < uboSize) { + // First allocation or material gained new properties — (re)allocate. + // Remove from maps first so they are never in a partially-updated state. + materialUbos.remove(entity); + materialUboSizes.remove(entity); + if (existingUbo != null) gpu.destroyBuffer(existingUbo); + existingUbo = gpu.createBuffer(uboSize, BufferUsage.UNIFORM, AccessPattern.DYNAMIC); + materialUbos.put(entity, existingUbo); + materialUboSizes.put(entity, uboSize); + } + var ubo = existingUbo; try (var w = gpu.writeBuffer(ubo)) { int offset = 0; @@ -151,6 +161,14 @@ public void uploadAndBindMaterial(MaterialData matData, Handle entity, Comman offset = align(offset, 16); BufferWriter.write(w.memory(), offset, value); offset += 16; + } else if (key.type() == dev.engine.core.math.Vec4.class || key.type() == dev.engine.core.math.Mat4.class) { + offset = align(offset, 16); + BufferWriter.write(w.memory(), offset, value); + offset += BufferWriter.sizeOf(key.type()); + } else if (key.type() == Vec2.class) { + offset = align(offset, 8); + BufferWriter.write(w.memory(), offset, value); + offset += BufferWriter.sizeOf(key.type()); } else { BufferWriter.write(w.memory(), offset, value); offset += BufferWriter.sizeOf(key.type()); @@ -166,10 +184,23 @@ public void close() { globalUbos.clear(); for (var ubo : materialUbos.values()) gpu.destroyBuffer(ubo); materialUbos.clear(); + materialUboSizes.clear(); for (var ubo : objectUbos.values()) gpu.destroyBuffer(ubo); objectUbos.clear(); } + /** + * Releases all per-entity GPU buffers for the given entity handle. + * Must be called when an entity is removed so that buffers are not leaked. + */ + public void removeEntity(Handle entity) { + var objUbo = objectUbos.remove(entity); + if (objUbo != null) gpu.destroyBuffer(objUbo); + var matUbo = materialUbos.remove(entity); + if (matUbo != null) gpu.destroyBuffer(matUbo); + materialUboSizes.remove(entity); + } + private static int align(int offset, int alignment) { return (offset + alignment - 1) & ~(alignment - 1); } diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/engine/BaseApplication.java b/graphics/common/src/main/java/dev/engine/graphics/common/engine/BaseApplication.java index 4ef49907..38638e13 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/engine/BaseApplication.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/engine/BaseApplication.java @@ -12,6 +12,7 @@ import dev.engine.graphics.common.Renderer; import dev.engine.graphics.window.WindowDescriptor; import dev.engine.graphics.window.WindowHandle; +import dev.engine.graphics.window.WindowProperty; import dev.engine.graphics.window.WindowToolkit; import dev.engine.ui.NkContext; import dev.engine.ui.NkInputBridge; @@ -46,7 +47,7 @@ * // Launch: * var config = EngineConfig.builder() * .platform(DesktopPlatform.builder().build()) - * .graphicsBackend(OpenGlBackend.factory(glBindings)) + * .graphics(new OpenGlConfig(toolkit, glBindings)) * .build(); * new MyGame().launch(config); * @@ -67,8 +68,7 @@ public abstract class BaseApplication { */ public void launch(EngineConfig config) { try { - var windowDesc = new WindowDescriptor( - config.windowTitle(), config.windowSize().x(), config.windowSize().y()); + var windowDesc = config.windowConfig().toDescriptor(); dev.engine.graphics.GraphicsBackend backend; if (config.graphics() != null) { backend = config.graphics().create(windowDesc); @@ -100,8 +100,19 @@ private void runInternal(EngineConfig config, GraphicsBackend backend) { // Default camera defaultCamera = engine.renderer().createCamera(); + // Set initial viewport from actual window dimensions + engine.renderer().setViewport(window.width(), window.height()); + window.show(); + // Apply window configuration properties + var wc = config.windowConfig(); + window.set(WindowProperty.RESIZABLE, wc.resizable()); + window.set(WindowProperty.DECORATED, wc.decorated()); + if (wc.vsync()) window.set(WindowProperty.VSYNC, true); + if (wc.fullscreen()) window.set(WindowProperty.FULLSCREEN, true); + if (wc.alwaysOnTop()) window.set(WindowProperty.ALWAYS_ON_TOP, true); + try { init(); @@ -128,7 +139,6 @@ private void runInternal(EngineConfig config, GraphicsBackend backend) { } } - engine.renderer().setViewport(window.width(), window.height()); engine.setInputEvents(inputEvents); // Feed input to debug UI diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/engine/Engine.java b/graphics/common/src/main/java/dev/engine/graphics/common/engine/Engine.java index 276e2027..71b077b3 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/engine/Engine.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/engine/Engine.java @@ -47,7 +47,6 @@ public class Engine { private final Renderer renderer; private final AbstractScene scene; private final Profiler profiler; - private final RenderStats renderStats; // Debug UI private final NkContext debugUi; @@ -72,7 +71,6 @@ public Engine(EngineConfig config, Platform platform, RenderDevice device) { // Profiler + stats this.profiler = new Profiler(); - this.renderStats = new RenderStats(); // Asset manager — synchronous by default, works on all platforms this.assets = new AssetManager(Runnable::run); @@ -108,7 +106,7 @@ public Engine(EngineConfig config, Platform platform, RenderDevice device) { public Renderer renderer() { return renderer; } public AbstractScene scene() { return scene; } public Profiler profiler() { return profiler; } - public RenderStats renderStats() { return renderStats; } + public RenderStats renderStats() { return renderer.renderStats(); } public EngineConfig config() { return config; } public NkContext debugUi() { return debugUi; } public DebugUiOverlay debugUiOverlay() { return debugUiOverlay; } @@ -141,7 +139,6 @@ public Handle registerMesh(MeshData data) { */ public void tick(double deltaSeconds) { profiler.newFrame(); - renderStats.reset(); totalTime += deltaSeconds; try (var scope = profiler.scope("logic")) { @@ -210,6 +207,7 @@ public void stop() { public void shutdown() { stop(); modules.shutdown(); + assets.shutdown(); debugUiOverlay.close(); renderer.close(); log.info("Engine shut down"); diff --git a/graphics/common/src/main/java/dev/engine/graphics/common/engine/EngineConfig.java b/graphics/common/src/main/java/dev/engine/graphics/common/engine/EngineConfig.java index cf49fe48..89a832d0 100644 --- a/graphics/common/src/main/java/dev/engine/graphics/common/engine/EngineConfig.java +++ b/graphics/common/src/main/java/dev/engine/graphics/common/engine/EngineConfig.java @@ -4,6 +4,7 @@ import dev.engine.core.scene.AbstractScene; import dev.engine.graphics.GraphicsBackendFactory; import dev.engine.graphics.GraphicsConfig; +import dev.engine.graphics.window.WindowConfig; /** * Configuration for the Engine. Determines threading mode, backend, window, scene type. @@ -22,14 +23,21 @@ public record EngineConfig( boolean headless, boolean threaded, - String windowTitle, - Vec2i windowSize, + WindowConfig windowConfig, AbstractScene scene, int maxFrames, Platform platform, GraphicsConfig graphics, GraphicsBackendFactory graphicsBackend ) { + // Backward-compat accessors + public String windowTitle() { return windowConfig != null ? windowConfig.title() : "Engine"; } + public Vec2i windowSize() { + return windowConfig != null + ? new Vec2i(windowConfig.width(), windowConfig.height()) + : new Vec2i(1280, 720); + } + public static Builder builder() { return new Builder(); } public static class Builder { @@ -37,8 +45,13 @@ public static class Builder { private boolean threaded = false; private String windowTitle = "Engine"; private Vec2i windowSize = new Vec2i(1280, 720); + private boolean windowResizable = true; + private boolean windowDecorated = true; + private boolean windowVsync = false; + private boolean windowFullscreen = false; + private boolean windowAlwaysOnTop = false; private AbstractScene scene = null; - private int maxFrames = 0; // 0 = unlimited + private int maxFrames = 0; private Platform platform = null; private GraphicsConfig graphics = null; private GraphicsBackendFactory graphicsBackend = null; @@ -48,6 +61,11 @@ public static class Builder { public Builder windowTitle(String title) { this.windowTitle = title; return this; } public Builder windowSize(Vec2i size) { this.windowSize = size; return this; } public Builder windowSize(int w, int h) { this.windowSize = new Vec2i(w, h); return this; } + public Builder windowResizable(boolean v) { this.windowResizable = v; return this; } + public Builder windowDecorated(boolean v) { this.windowDecorated = v; return this; } + public Builder windowVsync(boolean v) { this.windowVsync = v; return this; } + public Builder windowFullscreen(boolean v) { this.windowFullscreen = v; return this; } + public Builder windowAlwaysOnTop(boolean v) { this.windowAlwaysOnTop = v; return this; } public Builder scene(AbstractScene scene) { this.scene = scene; return this; } public Builder maxFrames(int maxFrames) { this.maxFrames = maxFrames; return this; } public Builder platform(Platform platform) { this.platform = platform; return this; } @@ -61,7 +79,10 @@ public EngineConfig build() { if (headless) { if (platform == null) platform = HeadlessPlatform.INSTANCE; } - return new EngineConfig(headless, threaded, windowTitle, windowSize, scene, maxFrames, platform, graphics, graphicsBackend); + var wc = new WindowConfig(windowTitle, windowSize.x(), windowSize.y(), + windowResizable, windowDecorated, windowVsync, windowFullscreen, windowAlwaysOnTop); + return new EngineConfig(headless, threaded, wc, scene, maxFrames, platform, graphics, graphicsBackend); } } } + diff --git a/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlBindings.java b/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlBindings.java index 15201fd7..1e327736 100644 --- a/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlBindings.java +++ b/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlBindings.java @@ -31,9 +31,21 @@ public interface GlBindings { // --- Blend factors --- int GL_ZERO = 0; int GL_ONE = 1; + int GL_SRC_COLOR = 0x0300; + int GL_ONE_MINUS_SRC_COLOR = 0x0301; int GL_SRC_ALPHA = 0x0302; int GL_ONE_MINUS_SRC_ALPHA = 0x0303; + int GL_DST_ALPHA = 0x0304; + int GL_ONE_MINUS_DST_ALPHA = 0x0305; int GL_DST_COLOR = 0x0306; + int GL_ONE_MINUS_DST_COLOR = 0x0307; + + // --- Blend equations --- + int GL_FUNC_ADD = 0x8006; + int GL_MIN = 0x8007; + int GL_MAX = 0x8008; + int GL_FUNC_SUBTRACT = 0x800A; + int GL_FUNC_REVERSE_SUBTRACT = 0x800B; // --- Capabilities --- int GL_DEPTH_TEST = 0x0B71; @@ -371,6 +383,16 @@ void glBlitNamedFramebuffer(int readFramebuffer, int drawFramebuffer, void glDisable(int cap); void glBlendFunc(int sfactor, int dfactor); + void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha); + void glBlendEquationSeparate(int modeRGB, int modeAlpha); + /** Per-draw-buffer blend function (GL 4.0+, required for MRT independent blending). */ + void glBlendFuncSeparatei(int buf, int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha); + /** Per-draw-buffer blend equation (GL 4.0+, required for MRT independent blending). */ + void glBlendEquationSeparatei(int buf, int modeRGB, int modeAlpha); + /** Per-draw-buffer enable (GL 3.0+). */ + void glEnablei(int cap, int index); + /** Per-draw-buffer disable (GL 3.0+). */ + void glDisablei(int cap, int index); void glCullFace(int mode); diff --git a/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlRenderDevice.java b/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlRenderDevice.java index 1d64a397..a58197f3 100644 --- a/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlRenderDevice.java +++ b/graphics/opengl/src/main/java/dev/engine/graphics/opengl/GlRenderDevice.java @@ -626,12 +626,7 @@ private void executeCommand(RenderCommand command) { else gl.glDisable(GlBindings.GL_DEPTH_TEST); } case RenderCommand.SetBlending cmd -> { - if (cmd.enabled()) { - gl.glEnable(GlBindings.GL_BLEND); - gl.glBlendFunc(GlBindings.GL_SRC_ALPHA, GlBindings.GL_ONE_MINUS_SRC_ALPHA); - } else { - gl.glDisable(GlBindings.GL_BLEND); - } + applyBlendMode(cmd.enabled() ? BlendMode.ALPHA : BlendMode.NONE); } case RenderCommand.SetCullFace cmd -> { if (cmd.enabled()) { @@ -669,6 +664,9 @@ private void executeCommand(RenderCommand command) { if (props.contains(RenderState.BLEND_MODE)) { applyBlendMode(props.get(RenderState.BLEND_MODE)); } + if (props.contains(RenderState.BLEND_MODES)) { + applyBlendModes(props.get(RenderState.BLEND_MODES)); + } if (props.contains(RenderState.CULL_MODE)) { applyCullMode(props.get(RenderState.CULL_MODE)); } @@ -954,22 +952,64 @@ private static int mapStencilOp(StencilOp op) { } private void applyBlendMode(BlendMode mode) { - if (mode == BlendMode.NONE) { + if (!mode.enabled()) { gl.glDisable(GlBindings.GL_BLEND); } else { gl.glEnable(GlBindings.GL_BLEND); - if (mode == BlendMode.ALPHA) { - gl.glBlendFunc(GlBindings.GL_SRC_ALPHA, GlBindings.GL_ONE_MINUS_SRC_ALPHA); - } else if (mode == BlendMode.ADDITIVE) { - gl.glBlendFunc(GlBindings.GL_SRC_ALPHA, GlBindings.GL_ONE); - } else if (mode == BlendMode.MULTIPLY) { - gl.glBlendFunc(GlBindings.GL_DST_COLOR, GlBindings.GL_ZERO); - } else if (mode == BlendMode.PREMULTIPLIED) { - gl.glBlendFunc(GlBindings.GL_ONE, GlBindings.GL_ONE_MINUS_SRC_ALPHA); + gl.glBlendFuncSeparate( + mapBlendFactor(mode.srcColorFactor()), mapBlendFactor(mode.dstColorFactor()), + mapBlendFactor(mode.srcAlphaFactor()), mapBlendFactor(mode.dstAlphaFactor())); + gl.glBlendEquationSeparate( + mapBlendEquation(mode.colorEquation()), + mapBlendEquation(mode.alphaEquation())); + } + } + + /** + * Applies per-draw-buffer blend modes for MRT (GL 4.0+ indexed blend). + * Index {@code i} of {@code modes} maps to draw buffer {@code i}; the last entry + * is repeated for any extra buffers. + */ + private void applyBlendModes(BlendMode[] modes) { + if (modes == null || modes.length == 0) return; + for (int i = 0; i < modes.length; i++) { + BlendMode mode = modes[i]; + if (!mode.enabled()) { + gl.glDisablei(GlBindings.GL_BLEND, i); + } else { + gl.glEnablei(GlBindings.GL_BLEND, i); + gl.glBlendFuncSeparatei(i, + mapBlendFactor(mode.srcColorFactor()), mapBlendFactor(mode.dstColorFactor()), + mapBlendFactor(mode.srcAlphaFactor()), mapBlendFactor(mode.dstAlphaFactor())); + gl.glBlendEquationSeparatei(i, + mapBlendEquation(mode.colorEquation()), + mapBlendEquation(mode.alphaEquation())); } } } + private int mapBlendFactor(BlendFactor factor) { + if (factor == BlendFactor.ZERO) return GlBindings.GL_ZERO; + if (factor == BlendFactor.ONE) return GlBindings.GL_ONE; + if (factor == BlendFactor.SRC_COLOR) return GlBindings.GL_SRC_COLOR; + if (factor == BlendFactor.ONE_MINUS_SRC_COLOR) return GlBindings.GL_ONE_MINUS_SRC_COLOR; + if (factor == BlendFactor.DST_COLOR) return GlBindings.GL_DST_COLOR; + if (factor == BlendFactor.ONE_MINUS_DST_COLOR) return GlBindings.GL_ONE_MINUS_DST_COLOR; + if (factor == BlendFactor.SRC_ALPHA) return GlBindings.GL_SRC_ALPHA; + if (factor == BlendFactor.ONE_MINUS_SRC_ALPHA) return GlBindings.GL_ONE_MINUS_SRC_ALPHA; + if (factor == BlendFactor.DST_ALPHA) return GlBindings.GL_DST_ALPHA; + if (factor == BlendFactor.ONE_MINUS_DST_ALPHA) return GlBindings.GL_ONE_MINUS_DST_ALPHA; + return GlBindings.GL_ZERO; + } + + private int mapBlendEquation(BlendEquation eq) { + if (eq == BlendEquation.SUBTRACT) return GlBindings.GL_FUNC_SUBTRACT; + if (eq == BlendEquation.REVERSE_SUBTRACT) return GlBindings.GL_FUNC_REVERSE_SUBTRACT; + if (eq == BlendEquation.MIN) return GlBindings.GL_MIN; + if (eq == BlendEquation.MAX) return GlBindings.GL_MAX; + return GlBindings.GL_FUNC_ADD; + } + private void applyCullMode(CullMode mode) { if (mode == CullMode.NONE) { gl.glDisable(GlBindings.GL_CULL_FACE); diff --git a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkBindings.java b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkBindings.java index d2efeb6a..f34538a5 100644 --- a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkBindings.java +++ b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkBindings.java @@ -201,26 +201,29 @@ long createRenderPass(long device, /** * Creates a graphics pipeline. * - * @param shaderModules shader module handles - * @param shaderStages corresponding VK_SHADER_STAGE_xxx flags - * @param vertexAttribLocations attribute locations - * @param vertexAttribFormats VK format per attribute - * @param vertexAttribOffsets byte offset per attribute - * @param vertexStride total vertex stride in bytes - * @param blendEnabled whether color blending is enabled - * @param srcColorFactor blend src color factor - * @param dstColorFactor blend dst color factor - * @param srcAlphaFactor blend src alpha factor - * @param dstAlphaFactor blend dst alpha factor - * @param wireframe if true, polygon mode = LINE - * @param dynamicStates VK_DYNAMIC_STATE_xxx values + * @param shaderModules shader module handles + * @param shaderStages corresponding VK_SHADER_STAGE_xxx flags + * @param vertexAttribLocations attribute locations + * @param vertexAttribFormats VK format per attribute + * @param vertexAttribOffsets byte offset per attribute + * @param vertexStride total vertex stride in bytes + * @param blendEnabled per-attachment blend enable flags (length = number of color attachments) + * @param srcColorFactors per-attachment src color blend factors + * @param dstColorFactors per-attachment dst color blend factors + * @param srcAlphaFactors per-attachment src alpha blend factors + * @param dstAlphaFactors per-attachment dst alpha blend factors + * @param colorBlendOps per-attachment color blend operations (VK_BLEND_OP_xxx) + * @param alphaBlendOps per-attachment alpha blend operations (VK_BLEND_OP_xxx) + * @param wireframe if true, polygon mode = LINE + * @param dynamicStates VK_DYNAMIC_STATE_xxx values */ long createGraphicsPipeline(long device, long renderPass, long pipelineLayout, long[] shaderModules, int[] shaderStages, int[] vertexAttribLocations, int[] vertexAttribFormats, int[] vertexAttribOffsets, int vertexStride, - boolean blendEnabled, int srcColorFactor, int dstColorFactor, - int srcAlphaFactor, int dstAlphaFactor, + boolean[] blendEnabled, int[] srcColorFactors, int[] dstColorFactors, + int[] srcAlphaFactors, int[] dstAlphaFactors, + int[] colorBlendOps, int[] alphaBlendOps, boolean wireframe, int[] dynamicStates); long createComputePipeline(long device, long pipelineLayout, long shaderModule); @@ -557,13 +560,16 @@ void queueSubmit(long queue, long cmd, long waitSemaphore, int VK_COMMAND_BUFFER_USAGE_ONE_TIME_SUBMIT_BIT = 0x00000001; // Blend factors - int VK_BLEND_FACTOR_ZERO = 0; - int VK_BLEND_FACTOR_ONE = 1; - int VK_BLEND_FACTOR_SRC_ALPHA = 6; - int VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA = 7; - int VK_BLEND_FACTOR_DST_COLOR = 8; - int VK_BLEND_FACTOR_DST_ALPHA = 10; - int VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA = 11; // not used currently but useful + int VK_BLEND_FACTOR_ZERO = 0; + int VK_BLEND_FACTOR_ONE = 1; + int VK_BLEND_FACTOR_SRC_COLOR = 2; + int VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR = 3; + int VK_BLEND_FACTOR_DST_COLOR = 4; + int VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR = 5; + int VK_BLEND_FACTOR_SRC_ALPHA = 6; + int VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA = 7; + int VK_BLEND_FACTOR_DST_ALPHA = 8; + int VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA = 9; // Compare ops int VK_COMPARE_OP_NEVER = 0; @@ -682,7 +688,11 @@ void queueSubmit(long queue, long cmd, long waitSemaphore, int VK_BORDER_COLOR_INT_OPAQUE_WHITE = 5; // Blend ops - int VK_BLEND_OP_ADD = 0; + int VK_BLEND_OP_ADD = 0; + int VK_BLEND_OP_SUBTRACT = 1; + int VK_BLEND_OP_REVERSE_SUBTRACT = 2; + int VK_BLEND_OP_MIN = 3; + int VK_BLEND_OP_MAX = 4; // Color component bits int VK_COLOR_COMPONENT_R_BIT = 0x00000001; diff --git a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkPipelineFactory.java b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkPipelineFactory.java index 2d153edd..9ba41d0d 100644 --- a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkPipelineFactory.java +++ b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkPipelineFactory.java @@ -4,8 +4,9 @@ import dev.engine.core.mesh.VertexFormat; import dev.engine.graphics.pipeline.ShaderBinary; import dev.engine.graphics.pipeline.ShaderStage; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import dev.engine.graphics.renderstate.BlendEquation; +import dev.engine.graphics.renderstate.BlendFactor; +import dev.engine.graphics.renderstate.BlendMode; import java.util.List; @@ -17,10 +18,19 @@ final class VkPipelineFactory { private VkPipelineFactory() {} /** - * Blend configuration for pipeline color attachment state. + * Blend configuration for a single pipeline color attachment. */ record BlendConfig(boolean enabled, int srcColorFactor, int dstColorFactor, - int srcAlphaFactor, int dstAlphaFactor) { + int srcAlphaFactor, int dstAlphaFactor, + int colorBlendOp, int alphaBlendOp) { + + /** Convenience constructor that defaults to VK_BLEND_OP_ADD for both equations. */ + BlendConfig(boolean enabled, int srcColorFactor, int dstColorFactor, + int srcAlphaFactor, int dstAlphaFactor) { + this(enabled, srcColorFactor, dstColorFactor, srcAlphaFactor, dstAlphaFactor, + VkBindings.VK_BLEND_OP_ADD, VkBindings.VK_BLEND_OP_ADD); + } + static final BlendConfig NONE = new BlendConfig(false, VkBindings.VK_BLEND_FACTOR_ONE, VkBindings.VK_BLEND_FACTOR_ZERO, VkBindings.VK_BLEND_FACTOR_ONE, VkBindings.VK_BLEND_FACTOR_ZERO); @@ -36,22 +46,70 @@ record BlendConfig(boolean enabled, int srcColorFactor, int dstColorFactor, static final BlendConfig PREMULTIPLIED = new BlendConfig(true, VkBindings.VK_BLEND_FACTOR_ONE, VkBindings.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, VkBindings.VK_BLEND_FACTOR_ONE, VkBindings.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA); + + static BlendConfig fromBlendMode(BlendMode mode) { + if (!mode.enabled()) return NONE; + return new BlendConfig(true, + mapFactor(mode.srcColorFactor()), mapFactor(mode.dstColorFactor()), + mapFactor(mode.srcAlphaFactor()), mapFactor(mode.dstAlphaFactor()), + mapEquation(mode.colorEquation()), mapEquation(mode.alphaEquation())); + } + + private static int mapFactor(BlendFactor factor) { + if (factor == BlendFactor.ZERO) return VkBindings.VK_BLEND_FACTOR_ZERO; + if (factor == BlendFactor.ONE) return VkBindings.VK_BLEND_FACTOR_ONE; + if (factor == BlendFactor.SRC_COLOR) return VkBindings.VK_BLEND_FACTOR_SRC_COLOR; + if (factor == BlendFactor.ONE_MINUS_SRC_COLOR) return VkBindings.VK_BLEND_FACTOR_ONE_MINUS_SRC_COLOR; + if (factor == BlendFactor.DST_COLOR) return VkBindings.VK_BLEND_FACTOR_DST_COLOR; + if (factor == BlendFactor.ONE_MINUS_DST_COLOR) return VkBindings.VK_BLEND_FACTOR_ONE_MINUS_DST_COLOR; + if (factor == BlendFactor.SRC_ALPHA) return VkBindings.VK_BLEND_FACTOR_SRC_ALPHA; + if (factor == BlendFactor.ONE_MINUS_SRC_ALPHA) return VkBindings.VK_BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + if (factor == BlendFactor.DST_ALPHA) return VkBindings.VK_BLEND_FACTOR_DST_ALPHA; + if (factor == BlendFactor.ONE_MINUS_DST_ALPHA) return VkBindings.VK_BLEND_FACTOR_ONE_MINUS_DST_ALPHA; + return VkBindings.VK_BLEND_FACTOR_ZERO; + } + + private static int mapEquation(BlendEquation eq) { + if (eq == BlendEquation.SUBTRACT) return VkBindings.VK_BLEND_OP_SUBTRACT; + if (eq == BlendEquation.REVERSE_SUBTRACT) return VkBindings.VK_BLEND_OP_REVERSE_SUBTRACT; + if (eq == BlendEquation.MIN) return VkBindings.VK_BLEND_OP_MIN; + if (eq == BlendEquation.MAX) return VkBindings.VK_BLEND_OP_MAX; + return VkBindings.VK_BLEND_OP_ADD; + } } static long create(VkBindings vk, long device, long renderPass, long pipelineLayout, List binaries, VertexFormat vertexFormat) { - return create(vk, device, renderPass, pipelineLayout, binaries, vertexFormat, BlendConfig.NONE, false); + return create(vk, device, renderPass, pipelineLayout, binaries, vertexFormat, + new BlendConfig[]{BlendConfig.NONE}, false); } static long create(VkBindings vk, long device, long renderPass, long pipelineLayout, List binaries, VertexFormat vertexFormat, BlendConfig blendConfig) { - return create(vk, device, renderPass, pipelineLayout, binaries, vertexFormat, blendConfig, false); + return create(vk, device, renderPass, pipelineLayout, binaries, vertexFormat, + new BlendConfig[]{blendConfig}, false); } static long create(VkBindings vk, long device, long renderPass, long pipelineLayout, List binaries, VertexFormat vertexFormat, BlendConfig blendConfig, boolean wireframe) { + return create(vk, device, renderPass, pipelineLayout, binaries, vertexFormat, + new BlendConfig[]{blendConfig}, wireframe); + } + + /** + * Creates a pipeline with per-attachment blend configs for MRT rendering. + * The {@code blendConfigs} array should contain one entry per color attachment. + * If shorter than the number of attachments, the last entry is repeated. + */ + static long create(VkBindings vk, long device, long renderPass, long pipelineLayout, + List binaries, VertexFormat vertexFormat, + BlendConfig[] blendConfigs, boolean wireframe) { + if (blendConfigs == null || blendConfigs.length == 0) { + blendConfigs = new BlendConfig[]{BlendConfig.NONE}; + } + // Create shader modules long[] modules = new long[binaries.size()]; int[] stages = new int[binaries.size()]; @@ -80,6 +138,26 @@ static long create(VkBindings vk, long device, long renderPass, long pipelineLay } } + // Flatten BlendConfig[] to parallel primitive arrays + int n = blendConfigs.length; + boolean[] blendEnabled = new boolean[n]; + int[] srcColor = new int[n]; + int[] dstColor = new int[n]; + int[] srcAlpha = new int[n]; + int[] dstAlpha = new int[n]; + int[] colorOps = new int[n]; + int[] alphaOps = new int[n]; + for (int i = 0; i < n; i++) { + var c = blendConfigs[i]; + blendEnabled[i] = c.enabled(); + srcColor[i] = c.srcColorFactor(); + dstColor[i] = c.dstColorFactor(); + srcAlpha[i] = c.srcAlphaFactor(); + dstAlpha[i] = c.dstAlphaFactor(); + colorOps[i] = c.colorBlendOp(); + alphaOps[i] = c.alphaBlendOp(); + } + // Dynamic states int[] dynamicStates = { VkBindings.VK_DYNAMIC_STATE_VIEWPORT, @@ -99,8 +177,8 @@ static long create(VkBindings vk, long device, long renderPass, long pipelineLay long pipeline = vk.createGraphicsPipeline(device, renderPass, pipelineLayout, modules, stages, attribLocations, attribFormats, attribOffsets, vertexStride, - blendConfig.enabled(), blendConfig.srcColorFactor(), blendConfig.dstColorFactor(), - blendConfig.srcAlphaFactor(), blendConfig.dstAlphaFactor(), + blendEnabled, srcColor, dstColor, srcAlpha, dstAlpha, + colorOps, alphaOps, wireframe, dynamicStates); // Destroy shader modules — no longer needed after pipeline creation diff --git a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkRenderDevice.java b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkRenderDevice.java index 14ff68e7..66d5f4ce 100644 --- a/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkRenderDevice.java +++ b/graphics/vulcan/src/main/java/dev/engine/graphics/vulkan/VkRenderDevice.java @@ -83,6 +83,8 @@ private record VkRenderTargetAllocation( private Handle currentBoundPipeline = null; private boolean currentWireframe = false; private BlendMode currentBlendMode = BlendMode.NONE; + /** Per-attachment blend modes for MRT; null means use {@code currentBlendMode} for all. */ + private BlendMode[] currentBlendModes = null; private record PipelineSpec(List binaries, VertexFormat vertexFormat) {} private final Map pipelineSpecs = new HashMap<>(); @@ -914,6 +916,7 @@ public void submit(dev.engine.graphics.command.CommandList commands) { currentBoundPipeline = bp.pipeline(); currentWireframe = false; currentBlendMode = BlendMode.NONE; + currentBlendModes = null; } } case dev.engine.graphics.command.RenderCommand.BindVertexBuffer bvb -> { @@ -1019,11 +1022,9 @@ public void submit(dev.engine.graphics.command.CommandList commands) { } case dev.engine.graphics.command.RenderCommand.SetBlending sb -> { if (currentBoundPipeline != null) { - var blendConfig = sb.enabled() - ? VkPipelineFactory.BlendConfig.ALPHA - : VkPipelineFactory.BlendConfig.NONE; currentBlendMode = sb.enabled() ? BlendMode.ALPHA : BlendMode.NONE; - rebindPipelineVariant(cmd, blendConfig, currentWireframe); + currentBlendModes = null; + rebindPipelineVariant(cmd, buildBlendConfigs(), currentWireframe); } } case dev.engine.graphics.command.RenderCommand.SetCullFace scf -> { @@ -1032,8 +1033,7 @@ public void submit(dev.engine.graphics.command.CommandList commands) { case dev.engine.graphics.command.RenderCommand.SetWireframe sw -> { currentWireframe = sw.enabled(); if (currentBoundPipeline != null) { - var blendConfig = mapBlendMode(currentBlendMode); - rebindPipelineVariant(cmd, blendConfig, currentWireframe); + rebindPipelineVariant(cmd, buildBlendConfigs(), currentWireframe); } } case dev.engine.graphics.command.RenderCommand.BindRenderTarget brt -> { @@ -1106,13 +1106,16 @@ public void submit(dev.engine.graphics.command.CommandList commands) { } if (props.contains(RenderState.BLEND_MODE) && currentBoundPipeline != null) { currentBlendMode = props.get(RenderState.BLEND_MODE); - var blendConfig = mapBlendMode(currentBlendMode); - rebindPipelineVariant(cmd, blendConfig, currentWireframe); + currentBlendModes = null; // clear per-attachment overrides + rebindPipelineVariant(cmd, buildBlendConfigs(), currentWireframe); + } + if (props.contains(RenderState.BLEND_MODES) && currentBoundPipeline != null) { + currentBlendModes = props.get(RenderState.BLEND_MODES); + rebindPipelineVariant(cmd, buildBlendConfigs(), currentWireframe); } if (props.contains(RenderState.WIREFRAME) && currentBoundPipeline != null) { currentWireframe = props.get(RenderState.WIREFRAME); - var blendConfig = mapBlendMode(currentBlendMode); - rebindPipelineVariant(cmd, blendConfig, currentWireframe); + rebindPipelineVariant(cmd, buildBlendConfigs(), currentWireframe); } } case dev.engine.graphics.command.RenderCommand.PushConstants(var data) -> { @@ -1177,28 +1180,45 @@ public void submit(dev.engine.graphics.command.CommandList commands) { } /** - * Rebinds the current pipeline with the given blend config and wireframe state, + * Builds the per-attachment {@link VkPipelineFactory.BlendConfig} array from the current blend state. + * If {@code currentBlendModes} is set, each entry maps to one attachment; otherwise + * {@code currentBlendMode} is used for all attachments (single-element array). + */ + private VkPipelineFactory.BlendConfig[] buildBlendConfigs() { + if (currentBlendModes != null && currentBlendModes.length > 0) { + VkPipelineFactory.BlendConfig[] configs = new VkPipelineFactory.BlendConfig[currentBlendModes.length]; + for (int i = 0; i < currentBlendModes.length; i++) { + configs[i] = VkPipelineFactory.BlendConfig.fromBlendMode(currentBlendModes[i]); + } + return configs; + } + return new VkPipelineFactory.BlendConfig[]{VkPipelineFactory.BlendConfig.fromBlendMode(currentBlendMode)}; + } + + /** + * Rebinds the current pipeline with the given per-attachment blend configs and wireframe state, * creating a new pipeline variant if needed. */ - private void rebindPipelineVariant(long cmd, VkPipelineFactory.BlendConfig blendConfig, boolean wireframe) { - var variantKey = currentBoundPipeline.index() + "_" + currentBlendMode.name() + "_" + wireframe; + private void rebindPipelineVariant(long cmd, VkPipelineFactory.BlendConfig[] blendConfigs, boolean wireframe) { + // Build a stable variant key from the blend config names + wireframe + var keyBuilder = new StringBuilder(); + keyBuilder.append(currentBoundPipeline.index()).append('_'); + if (currentBlendModes != null) { + for (var m : currentBlendModes) keyBuilder.append(m.name()).append(','); + } else { + keyBuilder.append(currentBlendMode.name()); + } + keyBuilder.append('_').append(wireframe); + var variantKey = keyBuilder.toString(); long variantPipeline = pipelineVariants.getOrCreate(variantKey, frameCounter.get(), k -> { var spec = pipelineSpecs.get(currentBoundPipeline.index()); if (spec == null) return pipelineRegistry.get(currentBoundPipeline); return VkPipelineFactory.create(vk, device, renderPass, - descriptorManager.pipelineLayout(), spec.binaries(), spec.vertexFormat(), blendConfig, wireframe); + descriptorManager.pipelineLayout(), spec.binaries(), spec.vertexFormat(), blendConfigs, wireframe); }); vk.cmdBindPipeline(cmd, VkBindings.VK_PIPELINE_BIND_POINT_GRAPHICS, variantPipeline); } - private VkPipelineFactory.BlendConfig mapBlendMode(BlendMode mode) { - if (mode == BlendMode.ALPHA) return VkPipelineFactory.BlendConfig.ALPHA; - if (mode == BlendMode.ADDITIVE) return VkPipelineFactory.BlendConfig.ADDITIVE; - if (mode == BlendMode.MULTIPLY) return VkPipelineFactory.BlendConfig.MULTIPLY; - if (mode == BlendMode.PREMULTIPLIED) return VkPipelineFactory.BlendConfig.PREMULTIPLIED; - return VkPipelineFactory.BlendConfig.NONE; - } - // --- Capabilities --- @Override diff --git a/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuBindings.java b/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuBindings.java index 4f08b5d6..293e4c37 100644 --- a/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuBindings.java +++ b/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuBindings.java @@ -530,15 +530,23 @@ record RenderPipelineDescriptor( int OPTIONAL_BOOL_TRUE = 1; // --- Blend factor --- - int BLEND_FACTOR_ZERO = 1; - int BLEND_FACTOR_ONE = 2; - int BLEND_FACTOR_SRC_ALPHA = 5; - int BLEND_FACTOR_ONE_MINUS_SRC_ALPHA = 6; - int BLEND_FACTOR_DST = 7; - int BLEND_FACTOR_DST_ALPHA = 9; + int BLEND_FACTOR_ZERO = 0; + int BLEND_FACTOR_ONE = 1; + int BLEND_FACTOR_SRC = 2; + int BLEND_FACTOR_ONE_MINUS_SRC = 3; + int BLEND_FACTOR_SRC_ALPHA = 4; + int BLEND_FACTOR_ONE_MINUS_SRC_ALPHA = 5; + int BLEND_FACTOR_DST = 6; + int BLEND_FACTOR_ONE_MINUS_DST = 7; + int BLEND_FACTOR_DST_ALPHA = 8; + int BLEND_FACTOR_ONE_MINUS_DST_ALPHA = 9; // --- Blend operation --- - int BLEND_OP_ADD = 1; + int BLEND_OP_ADD = 0; + int BLEND_OP_SUBTRACT = 1; + int BLEND_OP_REVERSE_SUBTRACT = 2; + int BLEND_OP_MIN = 3; + int BLEND_OP_MAX = 4; // --- Texture format --- int TEXTURE_FORMAT_R8_UNORM = 0x01; diff --git a/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuRenderDevice.java b/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuRenderDevice.java index 7d552923..b5aa8285 100644 --- a/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuRenderDevice.java +++ b/graphics/webgpu/src/main/java/dev/engine/graphics/webgpu/WgpuRenderDevice.java @@ -716,8 +716,8 @@ private long buildRenderPipeline(long pipelineLayout, long vertModule, ShaderSou stencilFront, stencilBack, currentColorTargetFormat(), - blend[0], blend[1], WgpuBindings.BLEND_OP_ADD, - blend[2], blend[3], WgpuBindings.BLEND_OP_ADD + blend[0], blend[1], mapBlendEquation(currentBlendMode.colorEquation()), + blend[2], blend[3], mapBlendEquation(currentBlendMode.alphaEquation()) ); return gpu.deviceCreateRenderPipeline(wgpuDevice, desc); @@ -754,28 +754,34 @@ private WgpuBindings.StencilFaceState buildStencilFaceState(boolean enabled) { /** Returns [colorSrc, colorDst, alphaSrc, alphaDst]. */ private static int[] getBlendFactors(BlendMode mode) { - if (mode == BlendMode.ALPHA) { - return new int[]{ - WgpuBindings.BLEND_FACTOR_SRC_ALPHA, WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA}; - } else if (mode == BlendMode.ADDITIVE) { - return new int[]{ - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ONE, - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ONE}; - } else if (mode == BlendMode.MULTIPLY) { - return new int[]{ - WgpuBindings.BLEND_FACTOR_DST, WgpuBindings.BLEND_FACTOR_ZERO, - WgpuBindings.BLEND_FACTOR_DST_ALPHA, WgpuBindings.BLEND_FACTOR_ZERO}; - } else if (mode == BlendMode.PREMULTIPLIED) { - return new int[]{ - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA, - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA}; - } else { - // NONE - return new int[]{ - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ZERO, - WgpuBindings.BLEND_FACTOR_ONE, WgpuBindings.BLEND_FACTOR_ZERO}; - } + return new int[]{ + mapBlendFactor(mode.srcColorFactor()), + mapBlendFactor(mode.dstColorFactor()), + mapBlendFactor(mode.srcAlphaFactor()), + mapBlendFactor(mode.dstAlphaFactor()) + }; + } + + private static int mapBlendFactor(dev.engine.graphics.renderstate.BlendFactor factor) { + if (factor == dev.engine.graphics.renderstate.BlendFactor.ZERO) return WgpuBindings.BLEND_FACTOR_ZERO; + if (factor == dev.engine.graphics.renderstate.BlendFactor.ONE) return WgpuBindings.BLEND_FACTOR_ONE; + if (factor == dev.engine.graphics.renderstate.BlendFactor.SRC_COLOR) return WgpuBindings.BLEND_FACTOR_SRC; + if (factor == dev.engine.graphics.renderstate.BlendFactor.ONE_MINUS_SRC_COLOR) return WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC; + if (factor == dev.engine.graphics.renderstate.BlendFactor.DST_COLOR) return WgpuBindings.BLEND_FACTOR_DST; + if (factor == dev.engine.graphics.renderstate.BlendFactor.ONE_MINUS_DST_COLOR) return WgpuBindings.BLEND_FACTOR_ONE_MINUS_DST; + if (factor == dev.engine.graphics.renderstate.BlendFactor.SRC_ALPHA) return WgpuBindings.BLEND_FACTOR_SRC_ALPHA; + if (factor == dev.engine.graphics.renderstate.BlendFactor.ONE_MINUS_SRC_ALPHA) return WgpuBindings.BLEND_FACTOR_ONE_MINUS_SRC_ALPHA; + if (factor == dev.engine.graphics.renderstate.BlendFactor.DST_ALPHA) return WgpuBindings.BLEND_FACTOR_DST_ALPHA; + if (factor == dev.engine.graphics.renderstate.BlendFactor.ONE_MINUS_DST_ALPHA) return WgpuBindings.BLEND_FACTOR_ONE_MINUS_DST_ALPHA; + return WgpuBindings.BLEND_FACTOR_ZERO; + } + + private static int mapBlendEquation(dev.engine.graphics.renderstate.BlendEquation eq) { + if (eq == dev.engine.graphics.renderstate.BlendEquation.SUBTRACT) return WgpuBindings.BLEND_OP_SUBTRACT; + if (eq == dev.engine.graphics.renderstate.BlendEquation.REVERSE_SUBTRACT) return WgpuBindings.BLEND_OP_REVERSE_SUBTRACT; + if (eq == dev.engine.graphics.renderstate.BlendEquation.MIN) return WgpuBindings.BLEND_OP_MIN; + if (eq == dev.engine.graphics.renderstate.BlendEquation.MAX) return WgpuBindings.BLEND_OP_MAX; + return WgpuBindings.BLEND_OP_ADD; } /** diff --git a/providers/graphics/desktop/lwjgl-gl/src/main/java/dev/engine/providers/lwjgl/graphics/opengl/LwjglGlBindings.java b/providers/graphics/desktop/lwjgl-gl/src/main/java/dev/engine/providers/lwjgl/graphics/opengl/LwjglGlBindings.java index ad8fd658..403b52c4 100644 --- a/providers/graphics/desktop/lwjgl-gl/src/main/java/dev/engine/providers/lwjgl/graphics/opengl/LwjglGlBindings.java +++ b/providers/graphics/desktop/lwjgl-gl/src/main/java/dev/engine/providers/lwjgl/graphics/opengl/LwjglGlBindings.java @@ -233,6 +233,12 @@ public class LwjglGlBindings implements GlBindings { @Override public void glDisable(int cap) { GL45.glDisable(cap); } @Override public void glBlendFunc(int sfactor, int dfactor) { GL45.glBlendFunc(sfactor, dfactor); } + @Override public void glBlendFuncSeparate(int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha) { GL45.glBlendFuncSeparate(sfactorRGB, dfactorRGB, sfactorAlpha, dfactorAlpha); } + @Override public void glBlendEquationSeparate(int modeRGB, int modeAlpha) { GL45.glBlendEquationSeparate(modeRGB, modeAlpha); } + @Override public void glBlendFuncSeparatei(int buf, int sfactorRGB, int dfactorRGB, int sfactorAlpha, int dfactorAlpha) { GL45.glBlendFuncSeparatei(buf, sfactorRGB, dfactorRGB, sfactorAlpha, dfactorAlpha); } + @Override public void glBlendEquationSeparatei(int buf, int modeRGB, int modeAlpha) { GL45.glBlendEquationSeparatei(buf, modeRGB, modeAlpha); } + @Override public void glEnablei(int cap, int index) { GL45.glEnablei(cap, index); } + @Override public void glDisablei(int cap, int index) { GL45.glDisablei(cap, index); } @Override public void glCullFace(int mode) { GL45.glCullFace(mode); } diff --git a/providers/graphics/desktop/lwjgl-vk/src/main/java/dev/engine/providers/lwjgl/graphics/vulkan/LwjglVkBindings.java b/providers/graphics/desktop/lwjgl-vk/src/main/java/dev/engine/providers/lwjgl/graphics/vulkan/LwjglVkBindings.java index 10cbcfc8..ab809b07 100644 --- a/providers/graphics/desktop/lwjgl-vk/src/main/java/dev/engine/providers/lwjgl/graphics/vulkan/LwjglVkBindings.java +++ b/providers/graphics/desktop/lwjgl-vk/src/main/java/dev/engine/providers/lwjgl/graphics/vulkan/LwjglVkBindings.java @@ -851,8 +851,9 @@ public long createGraphicsPipeline(long deviceHandle, long renderPass, long pipe long[] shaderModules, int[] shaderStages, int[] vertexAttribLocations, int[] vertexAttribFormats, int[] vertexAttribOffsets, int vertexStride, - boolean blendEnabled, int srcColorFactor, int dstColorFactor, - int srcAlphaFactor, int dstAlphaFactor, + boolean[] blendEnabled, int[] srcColorFactors, int[] dstColorFactors, + int[] srcAlphaFactors, int[] dstAlphaFactors, + int[] colorBlendOps, int[] alphaBlendOps, boolean wireframe, int[] dynamicStates) { var device = deviceCache.get(deviceHandle); try (var stack = stackPush()) { @@ -927,21 +928,26 @@ public long createGraphicsPipeline(long deviceHandle, long renderPass, long pipe .depthBoundsTestEnable(false) .stencilTestEnable(false); - var colorBlendAttachment = VkPipelineColorBlendAttachmentState.calloc(1, stack) - .colorWriteMask(VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT - | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT) - .blendEnable(blendEnabled) - .srcColorBlendFactor(srcColorFactor) - .dstColorBlendFactor(dstColorFactor) - .colorBlendOp(VK_BLEND_OP_ADD) - .srcAlphaBlendFactor(srcAlphaFactor) - .dstAlphaBlendFactor(dstAlphaFactor) - .alphaBlendOp(VK_BLEND_OP_ADD); + // One VkPipelineColorBlendAttachmentState per color attachment (MRT support) + int numAttachments = blendEnabled.length; + var colorBlendAttachments = VkPipelineColorBlendAttachmentState.calloc(numAttachments, stack); + for (int i = 0; i < numAttachments; i++) { + colorBlendAttachments.get(i) + .colorWriteMask(VK_COLOR_COMPONENT_R_BIT | VK_COLOR_COMPONENT_G_BIT + | VK_COLOR_COMPONENT_B_BIT | VK_COLOR_COMPONENT_A_BIT) + .blendEnable(blendEnabled[i]) + .srcColorBlendFactor(srcColorFactors[i]) + .dstColorBlendFactor(dstColorFactors[i]) + .colorBlendOp(colorBlendOps[i]) + .srcAlphaBlendFactor(srcAlphaFactors[i]) + .dstAlphaBlendFactor(dstAlphaFactors[i]) + .alphaBlendOp(alphaBlendOps[i]); + } var colorBlending = VkPipelineColorBlendStateCreateInfo.calloc(stack) .sType$Default() .logicOpEnable(false) - .pAttachments(colorBlendAttachment); + .pAttachments(colorBlendAttachments); var pipelineInfo = VkGraphicsPipelineCreateInfo.calloc(1, stack) .sType$Default() diff --git a/providers/windowing/desktop/lwjgl-glfw/src/main/java/dev/engine/windowing/glfw/GlfwWindowToolkit.java b/providers/windowing/desktop/lwjgl-glfw/src/main/java/dev/engine/windowing/glfw/GlfwWindowToolkit.java index a1b8fdea..22d908b2 100644 --- a/providers/windowing/desktop/lwjgl-glfw/src/main/java/dev/engine/windowing/glfw/GlfwWindowToolkit.java +++ b/providers/windowing/desktop/lwjgl-glfw/src/main/java/dev/engine/windowing/glfw/GlfwWindowToolkit.java @@ -183,6 +183,7 @@ public static class GlfwWindowHandle implements WindowHandle { properties.set(WindowProperty.RESIZABLE, true); properties.set(WindowProperty.DECORATED, true); properties.set(WindowProperty.FULLSCREEN, false); + properties.set(WindowProperty.ALWAYS_ON_TOP, false); } @Override @@ -269,6 +270,8 @@ public void set(PropertyKey key, T value) { } } else if (key == WindowProperty.SWAP_INTERVAL) { GLFW.glfwSwapInterval((Integer) value); + } else if (key == WindowProperty.ALWAYS_ON_TOP) { + GLFW.glfwSetWindowAttrib(handle, GLFW.GLFW_FLOATING, (Boolean) value ? GLFW.GLFW_TRUE : GLFW.GLFW_FALSE); } }