io7m | single-page | multi-page | JCoronado User Manual 3.0.0-beta0001

JCoronado User Manual 3.0.0-beta0001

JCoronado User Manual 3.0.0-beta0001
CREATOR Mark Raynsford
DATE 2026-09-26T09:59:42Z
DESCRIPTION A type-safe Java frontend for the Vulkan API.
IDENTIFIER a77de65d-4b33-4122-a6e4-f29a0057eff7
LANGUAGE en
RIGHTS Public Domain
TITLE JCoronado User Manual 3.0.0-beta0001

Table Of Contents

The jcoronado package provides a very thin layer over the Vulkan API that intends to provide some degree of memory and type safety. The intention of the package is to make Vulkan feel like a Java API, without sacrificing performance. Internally, the package uses the excellent LWJGL3 Vulkan bindings, and adds a thin layer of immutable types and interfaces.

1.2.1. Features

  • Type-safe Vulkan frontend.
  • Strong separation of API and implementation to allow for switching to different bindings at compile-time.
  • Extensive use of try-with-resources to prevent resource leaks.
  • Strongly-typed interfaces with a heavy emphasis on immutable value types.
  • Type-safe extension mechanism.
  • Fully documented (JavaDoc).
  • Example code included.
  • OSGi-ready.
  • JPMS-ready.
  • ISC license.
The jcoronado package currently targets Vulkan 1.4 and up. Some optional device features are required by the package.
The package requires the synchronization2 feature to be available and enabled. This is necessary to avoid having a lot of branching code paths around queue submission and render passes.
At the time of writing, according to the Vulkan hardware database , this feature is available on 99.82%of hardware.
The com.io7m.jcoronado.utility.allocation_tracker module provides a simple implementation of the VulkanHostAllocatorType interface that simply delegates an existing allocator (such as jemalloc) but also tracks the current amount of memory allocated for every allocation type.
Simply instantiate a VulkanHostAllocatorTracker instance and use it anywhere the API accepts a VulkanHostAllocatorType.
The com.io7m.jcoronado.utility.swapchain module provides a utility for managing the swapchain correctly.
Swapchain management is notoriously difficult, with many pitfalls and sharp edges. The JCSwapchainManager class provides a class for correctly creating swapchains, automatically recreating them if they become suboptimal or out-of-date, and acquiring and presenting images.
The class requires the use of the VK_EXT_swapchain_maintenance1 device extension to fix serious design flaws in the original VK_KHR_swapchainAPI.
Briefly, create a swapchain:

2.2.5. Creating a swap chain

final var swapChainManager =
  resources.add(
    JCSwapchainManager.create(
      JCSwapchainConfiguration.builder()
        .setDevice(device)
        .setGraphicsQueue(graphicsQueue)
        .setPresentationQueue(presentationQueue)
        .setSurface(surface)
        .setExtentSupplier(windowExtent::get)
        .setSurfaceExtension(khrSurfaceExt)
        .setSwapChainExtension(khrSwapchainExt)
        .addSurfaceAlphaFlags(VK_COMPOSITE_ALPHA_OPAQUE_BIT_KHR)
        .addImageUsageFlags(VK_IMAGE_USAGE_COLOR_ATTACHMENT_BIT)
        .addImageUsageFlags(VK_IMAGE_USAGE_TRANSFER_DST_BIT)
        .addPreferredModes(VK_PRESENT_MODE_MAILBOX_KHR)
        .addPreferredModes(VK_PRESENT_MODE_FIFO_KHR)
        .addPreferredModes(VK_PRESENT_MODE_IMMEDIATE_KHR)
        .build()
    )
  );
Acquire images in the rendering loop:

2.2.7. Acquiring and presenting images

final int framesInFlight = 2;

int frameNumber = 0;
while (rendering) {
  final var frameIndex =
    new JCSwapchainFrameIndex(frameNumber % framesInFlight);
  final var frame = swapChainManager.acquire(frameIndex);

  // Record the frame's commands into a command buffer (see "Command Pools").

  final var submitInfo =
    JCSwapchainSubmitInfo.builder()
      .addCommandBuffers(commandBuffer)
      .build();

  frame.submitAndPresent(submitInfo);
  ++frameNumber;
}
When an image is acquired or presented, the current swapchain may be detected as being suboptimal or out-of-date. When this happens, a new swapchain is created internally and the old one is (eventually) deleted.
On many operating systems, dragging a window's resize box can result in a flurry of updates that will result in hundreds of swapchain instances being created and deleted. For best results, disable rendering during window resize events. As an example, when using GLFW:

2.2.10. A resize-aware render loop (GLFW)

AtomicBoolean windowIsResizing;
AtomicReference<VulkanExtent2D> windowExtent;

GLFW.glfwSetWindowSizeCallback(
  window,
  GLFWWindowSizeCallback.create((_, _, _) -> {
    windowIsResizing.set(true);
  })
);

GLFW.glfwSetFramebufferSizeCallback(
  window,
  GLFWFramebufferSizeCallback.create((_, width, height) -> {
    windowExtent.set(
      VulkanExtent2D.builder()
        .setWidth(width)
        .setHeight(height)
        .build());
  })
);

final int framesInFlight = 2;
int frameNumber = 0;
while (rendering) {
  this.windowIsResizing.set(false);
  GLFW.glfwPollEvents();

  if (!windowIsResizing.get()) {
    final var frameIndex =
      new JCSwapchainFrameIndex(frameNumber % framesInFlight);
    final var frame = swapChainManager.acquire(frameIndex);

    // Record commands, then submit and present:
    final var submitInfo =
      JCSwapchainSubmitInfo.builder()
        .addCommandBuffers(commandBuffer)
        .build();
    frame.submitAndPresent(submitInfo);
    ++frameNumber;
  } else {
    pauseOneFrame();
  }
}
By avoiding rendering during window resizes, we effectively avoid creating and destroying swapchains for the intermediate window sizes. When the window eventually stops resizing, the next acquisition will detect the new extent reported by the supplier, and a suitable swapchain will automatically be created for the final size.
The com.io7m.jcoronado.utility.command_pools module provides a convenient and correct utility to manage command pools and buffers.
The command buffer API in Vulkan is fairly poorly designed in the sense that a command buffer has a complex lifecycle that doesn't really work well with the standard try-with-resources resource management. A command buffer is allocated, populated with commands, and is then submitted to a queue for execution. A command buffer should not be touched until the execution has completed on the GPU, so it therefore makes sense for command buffers to implement AutoCloseable so that the reference to the command buffer can be discarded after submission. This would work well except that, despite being allocated from a pool, command buffers still need to be individually deallocated, and deallocation is only safe after the command buffer has finished executing.
The VulkanCommandPoolHandler utility creates a command pool for each frame-in-flight, and tracks command buffers that are allocated from each pool. At the start of each frame, the respective command pool is reset and all command buffers allocated from that pool are automatically deallocated. The utility depends on the fact that if there are F frames in flight, and the current frame number is N, then it is safe to deallocate command buffers from frame M = N - F.

2.3.4. Managing per-frame command pools

long frameNumber = ...;
int framesInFlight = 3;

final var commandPools =
  VulkanCommandPoolHandler.create(
    device,
    index -> "CommandPool[%d]".formatted(index),
    Set.of(),
    graphicsQueue.queueFamilyIndex(),
    framesInFlight
  );

while (true) {
  final var poolFrame = commandPools.beginFrame(frameNumber % framesInFlight);
  try (final var commandBuffer =
    poolFrame.createCommandBuffer(VK_COMMAND_BUFFER_LEVEL_PRIMARY)) {
    ...
  }
}

// On shutdown
commandPools.close();
The com.io7m.jcoronado.utility.prometheus module provides a server component that exposes Prometheus metrics. This can be used, along with an appropriate Prometheus client, to get a real-time view of all Vulkan resources used by an application.
Objects must be registered with the server to appear in metrics. Objects will automatically be deregistered when closed.

2.4.3. Starting a metrics server

VulkanInstanceType instance;
VulkanLogicalDeviceType device;
VMAAllocatorType allocator;

try (var server = VulkanMetricsServers.start("localhost", 8989)) {
  server.registerInstance(instance);
  server.registerLogicalDevice(device);
  server.registerAllocator(allocator);
  ...
}
An example of the metrics output:

2.4.5. Example metrics output

$ curl http://localhost:8989
# TYPE vulkan_device_binary_semaphores gauge
# HELP vulkan_device_binary_semaphores Number of live binary semaphores
vulkan_device_binary_semaphores{device_id="0",instance_id="0"} 8.0
# TYPE vulkan_device_buffer_views gauge
# HELP vulkan_device_buffer_views Number of live buffer views
vulkan_device_buffer_views{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_buffers gauge
# HELP vulkan_device_buffers Number of live buffer objects
vulkan_device_buffers{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_command_buffers gauge
# HELP vulkan_device_command_buffers Number of live command buffers
vulkan_device_command_buffers{device_id="0",instance_id="0"} 4.0
# TYPE vulkan_device_command_pools gauge
# HELP vulkan_device_command_pools Number of live command pools
vulkan_device_command_pools{device_id="0",instance_id="0"} 4.0
# TYPE vulkan_device_compute_pipelines gauge
# HELP vulkan_device_compute_pipelines Number of live compute pipelines
vulkan_device_compute_pipelines{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_descriptor_pools gauge
# HELP vulkan_device_descriptor_pools Number of live descriptor pools
vulkan_device_descriptor_pools{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_descriptor_set_layouts gauge
# HELP vulkan_device_descriptor_set_layouts Number of live descriptor set layouts
vulkan_device_descriptor_set_layouts{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_events gauge
# HELP vulkan_device_events Number of live event objects
vulkan_device_events{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_fences gauge
# HELP vulkan_device_fences Number of live fence objects
vulkan_device_fences{device_id="0",instance_id="0"} 4.0
# TYPE vulkan_device_framebuffers gauge
# HELP vulkan_device_framebuffers Number of live framebuffers
vulkan_device_framebuffers{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_graphics_pipelines gauge
# HELP vulkan_device_graphics_pipelines Number of live graphics pipelines
vulkan_device_graphics_pipelines{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_image_views gauge
# HELP vulkan_device_image_views Number of live image views
vulkan_device_image_views{device_id="0",instance_id="0"} 4.0
# TYPE vulkan_device_images gauge
# HELP vulkan_device_images Number of live image objects
vulkan_device_images{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_pipeline_caches gauge
# HELP vulkan_device_pipeline_caches Number of live pipeline caches
vulkan_device_pipeline_caches{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_pipeline_layouts gauge
# HELP vulkan_device_pipeline_layouts Number of live pipeline layouts
vulkan_device_pipeline_layouts{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_query_pools gauge
# HELP vulkan_device_query_pools Number of live query pools
vulkan_device_query_pools{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_render_passes gauge
# HELP vulkan_device_render_passes Number of live render passes
vulkan_device_render_passes{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_samplers gauge
# HELP vulkan_device_samplers Number of live sampler objects
vulkan_device_samplers{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_device_timeline_semaphores gauge
# HELP vulkan_device_timeline_semaphores Number of live timeline semaphores
vulkan_device_timeline_semaphores{device_id="0",instance_id="0"} 0.0
# TYPE vulkan_host_allocator_cache_scope_bytes gauge
# UNIT vulkan_host_allocator_cache_scope_bytes bytes
# HELP vulkan_host_allocator_cache_scope_bytes Octets allocated at cache scope
vulkan_host_allocator_cache_scope_bytes{instance_id="0"} 192.0
# TYPE vulkan_host_allocator_command_scope_bytes gauge
# UNIT vulkan_host_allocator_command_scope_bytes bytes
# HELP vulkan_host_allocator_command_scope_bytes Octets allocated at command scope
vulkan_host_allocator_command_scope_bytes{instance_id="0"} 0.0
# TYPE vulkan_host_allocator_device_scope_bytes gauge
# UNIT vulkan_host_allocator_device_scope_bytes bytes
# HELP vulkan_host_allocator_device_scope_bytes Octets allocated at device scope
vulkan_host_allocator_device_scope_bytes{instance_id="0"} 71644.0
# TYPE vulkan_host_allocator_instance_scope_bytes gauge
# UNIT vulkan_host_allocator_instance_scope_bytes bytes
# HELP vulkan_host_allocator_instance_scope_bytes Octets allocated at instance scope
vulkan_host_allocator_instance_scope_bytes{instance_id="0"} 278711.0
# TYPE vulkan_host_allocator_object_scope_bytes gauge
# UNIT vulkan_host_allocator_object_scope_bytes bytes
# HELP vulkan_host_allocator_object_scope_bytes Octets allocated at object scope
vulkan_host_allocator_object_scope_bytes{instance_id="0"} 99632.0
# EOF
The Java API is documented in the included JavaDoc.
io7m | single-page | multi-page | JCoronado User Manual 3.0.0-beta0001