Skip to content

Commit

Permalink
Add PerFrameBuffer
Browse files Browse the repository at this point in the history
  • Loading branch information
knokko committed Dec 24, 2024
1 parent 1472cd2 commit 5bdb2a8
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 2 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ repositories {
...
dependencies {
...
implementation 'com.github.knokko:vk-boiler:v4.3.0'
implementation 'com.github.knokko:vk-boiler:v4.4.0'
}
```

Expand All @@ -167,7 +167,7 @@ dependencies {
<dependency>
<groupId>com.github.knokko</groupId>
<artifactId>vk-boiler</artifactId>
<version>v4.3.0</version>
<version>v4.4.0</version>
</dependency>
```

Expand Down
6 changes: 6 additions & 0 deletions docs/methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ method of a `MappedVkbBuffer`. It provides `byteBuffer()`,
is backed by the buffer range. It also provides a `range(...)` method to
create a corresponding `VkbBufferRange`.

### PerFrameBuffer
The `PerFrameBuffer` wraps a `MappedVkbBufferRange`, and uses it to manage
one-time-only data that you use every frame, and whose memory space can be
reused after `numberOfFramesInFlight` frames. You can use it to easily
share such space with multiple independent renderers.

### Encoding/decoding images
You can use `boiler.buffers.encodeBufferedImageRGBA(...)` to encode/store a
`BufferedImage` in a `MappedVkbBuffer` in RGBA8 format. You can use this to
Expand Down
119 changes: 119 additions & 0 deletions src/main/java/com/github/knokko/boiler/buffers/PerFrameBuffer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package com.github.knokko.boiler.buffers;

import com.github.knokko.boiler.exceptions.PerFrameOverflowException;

import java.util.HashMap;
import java.util.Map;

/**
* Utility class to facilitate the sharing of one-time-use per-frame buffer storage between different renderers.
*
* <p>
* To use this class, you should create 1 instance (per rendering thread) of this class, which should usually be
* reused until the application is closed.
* </p>
*
* <p>
* At the start of this frame, you should call the {@link #startFrame(int)} method, where
* {@code frameIndex = frameCounter % numberOfFramesInFlight}, and {@code frameCounter} is a variable that is
* incremented once per frame.
* </p>
*
* <p>
* Whenever a renderer needs some one-time-use per-frame data, it should call the {@link #allocate(long, long)}
* method to request a piece of data.
* </p>
*/
public class PerFrameBuffer {

/**
* The buffer/memory range that will be used by this per-frame buffer. Pieces of this range will be returned by
* the {@link #allocate(long, long)} method.
*/
public final MappedVkbBufferRange range;

private final Map<Integer, Long> limits = new HashMap<>();

private long currentOffset, currentLimit;

/**
* Constructs a new per-frame buffer, which you should usually do only once.
* @param range See {@link #range}
*/
public PerFrameBuffer(MappedVkbBufferRange range) {
this.range = range;
}

/**
* You should call this at the start of each frame. When you:
* <ol>
* <li>Call {@code startFrame(i)}</li>
* <li>Call {@link #allocate(long, long)} a couple of times</li>
* <li>Call {@code startFrame(j)}</li>
* </ol>
* All space used for the allocations between frame-in-flight {@code i} and frame-in-flight {@code j} can be reused
* after you call {@code startFrame(i)} again.
* @param frameIndex {@code frameCounter % numberOfFramesInFlight}, when you start your {@code frameCounter}th
* frame.
*/
public void startFrame(int frameIndex) {
limits.remove(frameIndex);

long nextLimit = Long.MAX_VALUE;
for (long candidate : limits.values()) {
if (candidate >= currentOffset && candidate < nextLimit) nextLimit = candidate;
}

if (nextLimit == Long.MAX_VALUE) {
for (long candidate : limits.values()) {
if (candidate < nextLimit) nextLimit = candidate;
}
}

if (nextLimit == Long.MAX_VALUE) {
nextLimit = currentOffset - 1;
}

if (nextLimit < 0) nextLimit = range.size();

currentLimit = nextLimit;
limits.put(frameIndex, currentOffset - 1);
}

private void align(long alignment) {
long fullOffset = range.offset() + currentOffset;
if (fullOffset % alignment == 0L) return;

fullOffset = (1L + fullOffset / alignment) * alignment;
currentOffset = fullOffset - range.offset();
}

/**
* Allocates {@code byteSize} bytes of memory, ensures that the {@link MappedVkbBufferRange#offset()} of the
* returned buffer range will be a multiple of {@code alignment}
* @param byteSize The size of the memory to claim, in bytes
* @param alignment The alignment of the memory to claim, in bytes
* @return The allocated buffer range
*/
public MappedVkbBufferRange allocate(long byteSize, long alignment) {
align(alignment);
long nextOffset = currentOffset + byteSize;

if (currentOffset > currentLimit && nextOffset > range.size()) {
currentOffset = 0L;
align(alignment);
if (currentOffset >= currentLimit) {
throw new PerFrameOverflowException("PerFrameBuffer overflow case 2: byteSize is " + byteSize);
}
nextOffset = currentOffset + byteSize;
}

if (currentOffset <= currentLimit && nextOffset > currentLimit) {
throw new PerFrameOverflowException("PerFrameBuffer overflow case 1: byteSize is " + byteSize);
}

var result = range.buffer().mappedRange(range.offset() + currentOffset, byteSize);
currentOffset = nextOffset;
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.github.knokko.boiler.exceptions;

/**
* This exception will be thrown when {@link com.github.knokko.boiler.buffers.PerFrameBuffer#allocate(long, long)}
* fails because there is not enough space in the per-frame buffer. This can basically have 2 possible causes:
* <ul>
* <li>The per-frame buffer is too small</li>
* <li>
* You forgot to call {@link com.github.knokko.boiler.buffers.PerFrameBuffer#startFrame(int)},
* preventing it from reclaiming memory from previous frames.
* </li>
* </ul>
*/
public class PerFrameOverflowException extends RuntimeException {

public PerFrameOverflowException(String message) {
super(message);
}
}
121 changes: 121 additions & 0 deletions src/test/java/com/github/knokko/boiler/buffers/TestPerFrameBuffer.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package com.github.knokko.boiler.buffers;

import com.github.knokko.boiler.BoilerInstance;
import com.github.knokko.boiler.builders.BoilerBuilder;
import com.github.knokko.boiler.exceptions.PerFrameOverflowException;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestInstance;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.lwjgl.vulkan.VK10.VK_API_VERSION_1_0;
import static org.lwjgl.vulkan.VK10.VK_BUFFER_USAGE_VERTEX_BUFFER_BIT;

@TestInstance(TestInstance.Lifecycle.PER_CLASS)
public class TestPerFrameBuffer {

private final BoilerInstance instance = new BoilerBuilder(
VK_API_VERSION_1_0, "TestPerFrameBuffer", 1
).validation().forbidValidationErrors().build();

@Test
public void testBasic() {
var buffer = instance.buffers.createMapped(10, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.fullMappedRange());

perFrame.startFrame(0);
assertEquals(0L, perFrame.allocate(2, 3).offset());
assertEquals(2L, perFrame.allocate(2, 2).offset());
assertEquals(5L, perFrame.allocate(1, 5).offset());

perFrame.startFrame(1);
assertEquals(6L, perFrame.allocate(3, 3).offset());
assertEquals(9L, perFrame.allocate(1, 3).offset());

perFrame.startFrame(0);
assertEquals(0L, perFrame.allocate(3, 1).offset());

buffer.destroy(instance);
}

@Test
public void testOverflowFirstFrame() {
var buffer = instance.buffers.createMapped(10, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.fullMappedRange());

perFrame.startFrame(0);
assertEquals(0L, perFrame.allocate(6, 3).offset());
assertThrows(PerFrameOverflowException.class, () -> perFrame.allocate(6, 1));

buffer.destroy(instance);
}

@Test
public void testOverflowSecondFrame() {
var buffer = instance.buffers.createMapped(10, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.fullMappedRange());

perFrame.startFrame(0);
assertEquals(0L, perFrame.allocate(6, 3).offset());
perFrame.startFrame(1);
assertThrows(PerFrameOverflowException.class, () -> perFrame.allocate(6, 1));

buffer.destroy(instance);
}

@Test
public void testRespectAlignmentWithRangeOffset() {
var buffer = instance.buffers.createMapped(15, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.mappedRange(3, 8));

perFrame.startFrame(1);
assertEquals(5L, perFrame.allocate(3, 5).offset());
assertEquals(8L, perFrame.allocate(2, 4).offset());

perFrame.startFrame(1);
assertEquals(5L, perFrame.allocate(3, 5).offset());

buffer.destroy(instance);
}

@Test
public void testWrapAlignmentOverflow1() {
var buffer = instance.buffers.createMapped(15, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.mappedRange(1, 10));

perFrame.startFrame(0);
assertEquals(1L, perFrame.allocate(3, 1).offset());

perFrame.startFrame(1);
assertEquals(5L, perFrame.allocate(3, 5).offset());
assertThrows(PerFrameOverflowException.class, () -> perFrame.allocate(3, 5));

buffer.destroy(instance);
}

@Test
public void testWrapAlignmentOverflow2() {
var buffer = instance.buffers.createMapped(15, VK_BUFFER_USAGE_VERTEX_BUFFER_BIT, "PerFrame");
var perFrame = new PerFrameBuffer(buffer.mappedRange(1, 10));

perFrame.startFrame(0);
assertEquals(1L, perFrame.allocate(2, 1).offset());

perFrame.startFrame(1);
assertEquals(3L, perFrame.allocate(2, 1).offset());

perFrame.startFrame(0);
assertEquals(5L, perFrame.allocate(3, 1).offset());

perFrame.startFrame(1);
assertThrows(PerFrameOverflowException.class, () -> perFrame.allocate(5, 5));

buffer.destroy(instance);
}

@AfterAll
public void tearDown() {
instance.destroyInitialObjects();
}
}

0 comments on commit 5bdb2a8

Please sign in to comment.