ImGuiRenderToTexture.kt
src/jvmMain/kotlin/afield/graphics/vulkan/imgui/ImGuiRenderToTexture.kt · 1031 lines
package afield.graphics.vulkan.imgui
Draws Dear ImGui (through imgui-java) into a Field render-to-texture target, so an ImGui interface can be shown as a texture on any surface, including a panel in VR.
ImGuiRenderToTexture owns the ImGui context, the font atlas and the vertex buffers, and records ImGui's draw lists into its target each frame, honouring clip rects with a dynamic scissor. Only the font atlas is bound as a texture; other ImGui texture ids are ignored. ImGuiPanel places such a texture in world space and feeds controller rays back to it as pointer input.
import afield.runtime.lifecycle.Generator
import afield.graphics.vulkan.Renderer
import afield.graphics.vulkan.buffers.VertexBuffer
import afield.graphics.vulkan.device.CommandBuffer
import afield.graphics.render.Module
import afield.graphics.render.ModulePointer
import afield.document.context.fromContext
import afield.graphics.render.parameters
import afield.graphics.vulkan.targets.RTTParameters
import afield.graphics.vulkan.targets.RenderTarget
import afield.graphics.vulkan.targets.RenderToTexture
import afield.graphics.texture.HasImageViewArray
import afield.graphics.texture.StreamingTexture
import afield.graphics.texture.TextureSampler
import afield.graphics.vulkan.util.MapOfGenerators
import afield.graphics.vulkan.util.Window
import afield.math.linalg.f
import imgui.*
import imgui.ImGui.text
import imgui.flag.ImGuiKey
import imgui.internal.ImGuiContext
import imgui.type.ImInt
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.lwjgl.glfw.GLFW
import org.lwjgl.system.MemoryStack
import org.lwjgl.system.MemoryUtil
import org.lwjgl.vulkan.*
import org.lwjgl.vulkan.VK10.*
import org.lwjgl.vulkan.VK13.*
import java.net.URI
import java.nio.ByteOrder
import java.util.*
import kotlin.math.max
import kotlin.math.roundToInt
class ImGuiRenderToTexture
Minimal imgui-java renderer that draws Dear ImGui into a Field RenderToTexture.
Notes / limitations (intentional for a first step): - Renders using Field's vertex-buffer convention (one buffer per attribute). - Supports ImGui clip rects via dynamic scissor. - Currently binds a single texture (the font atlas); additional texture IDs are ignored.
declaration — 59 lines, from line 58
class ImGuiRenderToTexture(
renderer: Renderer = fromContext(),
private val window: Window? = renderer.window,
parameters: RTTParameters.() -> Unit = {},
/**
* Supersample the font atlas: scales the rasterization size and keeps on-screen size stable via FontGlobalScale.
*/
var fontSupersample: Float = 4f,
/**
* If true, converts the current style colors to grayscale for a monochrome look.
*/
var monochromeTheme: Boolean = true,
/**
* When true, feeds window mouse/scroll into ImGui when no pointer override is provided.
* Set false when you inject pointers manually (e.g., from a stereo controller).
*/
var useWindowMouseEvents: Boolean = true,
/**
* Optional UI scale applied via DisplayFramebufferScale (acts like supersampling for layout).
*/
var displayScale: Float = 1f,
var ui: (ImGuiIO) -> Unit = {},
) : RenderToTexture(
renderer,
parameters = {
useDepth = false
format = VK_FORMAT_B8G8R8A8_SRGB
parameters()
}
) {
private val context: ImGuiContext = ImGui.createContext()
private val fontTexId = 1L
@Suppress("UNNECESSARY_LATEINIT")
private lateinit var fontTexture: StreamingTexture
private val fontSampler: TextureSampler
private val module: Module
@Volatile private var pendingFont: PendingFont? = null
private val uiGenerators = MapOfGenerators<ImGuiIO, Unit>()
@Volatile private var pointerOverride: PointerOverride? = null
private val pendingKeyEvents = ArrayDeque<KeyEvent>()
private val pendingChars = ArrayDeque<Int>()
private var removeKeyListener: (() -> Unit)? = null
private var removeCharListener: (() -> Unit)? = null
private var modCtrl = false
private var modShift = false
private var modAlt = false
private var modSuper = false
private var lastFrameNs = System.nanoTime()
private var drawData: ImDrawData? = null
private var vertexCapacity = 0
private var indexCapacity = 0
private var posBuffer: VertexBuffer? = null
private var uvBuffer: VertexBuffer? = null
private var colorBuffer: VertexBuffer? = null
private var indexBuffer: VertexBuffer? = null
init
body — 44 lines, from line 118
init {
val prev = ImGui.getCurrentContext()
ImGui.setCurrentContext(context)
try {
val io = ImGui.getIO()
io.setIniFilename(null)
io.setBackendPlatformName("field-glfw")
io.setBackendRendererName("field-vulkan-rtt")
io.setBackendUsingLegacyKeyArrays(0.toShort())
io.setBackendUsingLegacyNavInputArray(false)
io.setDisplayFramebufferScale(1f, 1f)
ImGui.styleColorsDark()
if (monochromeTheme) applyMonochromeStyle()
applyTightSpacing()
addLegacyUi("default-ui", ui)
fontTexture = rebuildFontTexture(fontData = null, sizePixels = DEFAULT_FONT_SIZE)
fontSampler = TextureSampler(renderer, mipLevels = 1, anisotropyEnable = false)
val provider = object : HasImageViewArray {
override val imageViewArray get() = listOf(fontTexture)
}
module = Module(
parameters(renderer) {
name = "imgui-rtt"
colorFormats = this@ImGuiRenderToTexture.colorFormats
color = colorFormats.size
depth = 0
depthTest = false
blend = true
vertex = imguiVertexShader
fragment = imguiFragmentShader
// For now we only support the built-in font atlas texture. Be permissive about the name to avoid
// reflection/name-mangling surprises (a common "blank output" failure mode).
textureResolver = { _ -> provider to fontSampler }
}
)
} finally {
ImGui.setCurrentContext(prev)
}
installInputCallbacks()
}
fun attachTo
Convenience: make sure this RTT is rendered every frame (as a preTarget) and blitted to the screen.
The most common "it's blank" failure mode is calling blitToScreen but never actually adding
this Target into the render graph. This helper does both.
fun attachTo(
target: RenderTarget = renderer.renderList,
name: String = "imgui-rtt",
blitName: String = "$name-blit",
): ModulePointer {
target.preTargets[name] = this
// No sourceRect: this used to pass Rect(0.0, 1.0, 1.0, -1.0) to cancel a vertical mirror in
// RenderToTexture.blitVertexShader. That shader now samples texel-aligned, so the identity
// crop is the correct one and a flip here would re-invert the panel.
return blitToScreen(renderer = renderer, target = target, name = blitName, blend = true)
}
fun cleanup()
fun cleanup() {
posBuffer?.cleanup()
uvBuffer?.cleanup()
colorBuffer?.cleanup()
indexBuffer?.cleanup()
fontSampler.cleanup()
fontTexture.cleanup()
module.cleanup()
removeKeyListener?.invoke()
removeKeyListener = null
removeCharListener?.invoke()
removeCharListener = null
val prev = ImGui.getCurrentContext()
ImGui.setCurrentContext(context)
try {
ImGui.destroyContext(context)
} finally {
ImGui.setCurrentContext(prev)
}
}
override fun prepareAdditionalResources(command: CommandBuffer, index: Int)
override fun prepareAdditionalResources(command: CommandBuffer, index: Int) {
val prev = ImGui.getCurrentContext()
ImGui.setCurrentContext(context)
try {
val io = ImGui.getIO()
pendingFont?.let { pf ->
rebuildFontTexture(pf.bytes, pf.sizePixels)
pendingFont = null
}
updateIo(io)
ImGui.newFrame()
uiGenerators.update(io)
ImGui.render()
val dd = ImGui.getDrawData()
drawData = dd
uploadDrawData(dd, command)
} finally {
ImGui.setCurrentContext(prev)
}
}
override fun renderAdditional(command: CommandBuffer, index: Int, stack: MemoryStack)
body — 380 lines, from line 226
override fun renderAdditional(command: CommandBuffer, index: Int, stack: MemoryStack) {
val dd = drawData ?: return
if (!dd.getValid()) return
if (dd.getTotalVtxCount() <= 0 || dd.getTotalIdxCount() <= 0) return
if (!module.valid) return
val pos = posBuffer ?: return
val uv = uvBuffer ?: return
val col = colorBuffer ?: return
val idx = indexBuffer ?: return
val cmd = command.vkCommandBuffer
// Ensure a predictable viewport for ImGui (Field flips Y via negative viewport height).
val viewport = VkViewport.calloc(1, stack)
viewport[0]
.x(0f)
.y(height.toFloat())
.width(width.toFloat())
.height(-height.toFloat())
.minDepth(0f)
.maxDepth(1f)
vkCmdSetViewport(cmd, 0, viewport)
vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, module.pipeline)
vkCmdSetDepthTestEnable(cmd, false)
if (renderer.device.supportsExtendedDynamicState) {
vkCmdSetPrimitiveTopology(cmd, VK_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST)
vkCmdSetCullMode(cmd, VK_CULL_MODE_NONE)
vkCmdSetFrontFace(cmd, VK_FRONT_FACE_CLOCKWISE)
}
if (renderer.device.supportsExtendedDynamicState3) {
EXTExtendedDynamicState3.vkCmdSetDepthClampEnableEXT(cmd, false)
}
// Bind descriptors (font texture)
module.uniforms.values.forEach { upd ->
upd.pushDescriptorSet(command, module.pipelineLayout, VK_PIPELINE_BIND_POINT_GRAPHICS)
}
// Push constants: map ImGui coordinates (origin top-left) into Field's flipped viewport (OpenGL-style NDC).
val displayW = dd.getDisplaySizeX()
val displayH = dd.getDisplaySizeY()
if (displayW <= 0f || displayH <= 0f) return
val scaleX = 2f / displayW
val scaleY = -2f / displayH
val translateX = -1f - dd.getDisplayPosX() * scaleX
val translateY = 1f - dd.getDisplayPosY() * scaleY
val pc = stack.malloc(16)
pc.putFloat(0, scaleX)
pc.putFloat(4, scaleY)
pc.putFloat(8, translateX)
pc.putFloat(12, translateY)
vkCmdPushConstants(
cmd,
module.pipelineLayout,
VK_SHADER_STAGE_VERTEX_BIT or VK_SHADER_STAGE_FRAGMENT_BIT,
0,
pc
)
// Vertex/index buffers
run {
val inputs = module.vi.inputs
require(inputs.size == 3) { "Unexpected ImGui vertex input count ${inputs.size} (expected 3)" }
val vb = stack.mallocLong(inputs.size)
val offs = stack.mallocLong(inputs.size)
inputs.forEachIndexed { bindingIndex, input ->
val handle = when (input.name) {
"inPosition" -> pos.dest.buffer
"inUV" -> uv.dest.buffer
"inColor" -> col.dest.buffer
else -> error("Unexpected ImGui vertex input '${input.name}'")
}
vb.put(bindingIndex, handle)
offs.put(bindingIndex, 0L)
}
vkCmdBindVertexBuffers(cmd, 0, vb, offs)
}
vkCmdBindIndexBuffer(cmd, idx.dest.buffer, 0L, VK_INDEX_TYPE_UINT32)
val clipOffX = dd.getDisplayPosX()
val clipOffY = dd.getDisplayPosY()
val clipScaleX = dd.getFramebufferScaleX()
val clipScaleY = dd.getFramebufferScaleY()
val fbW = (dd.getDisplaySizeX() * clipScaleX).toInt()
val fbH = (dd.getDisplaySizeY() * clipScaleY).toInt()
if (fbW <= 0 || fbH <= 0) return
val scissor = VkRect2D.calloc(1, stack)
val scissorOffset = scissor[0].offset()
val scissorExtent = scissor[0].extent()
var globalVtxOffset = 0
var globalIdxOffset = 0
val cmdLists = dd.getCmdListsCount()
for (listIndex in 0 until cmdLists) {
val cmdCount = dd.getCmdListCmdBufferSize(listIndex)
for (cmdIndex in 0 until cmdCount) {
val elemCount = dd.getCmdListCmdBufferElemCount(listIndex, cmdIndex)
if (elemCount <= 0) continue
// Texture IDs other than the font atlas are currently ignored.
// val textureId = dd.getCmdListCmdBufferTextureId(listIndex, cmdIndex)
val clip = dd.getCmdListCmdBufferClipRect(listIndex, cmdIndex)
val clipMinX = (clip.x - clipOffX) * clipScaleX
val clipMinY = (clip.y - clipOffY) * clipScaleY
val clipMaxX = (clip.z - clipOffX) * clipScaleX
val clipMaxY = (clip.w - clipOffY) * clipScaleY
if (clipMaxX <= clipMinX || clipMaxY <= clipMinY) continue
val x0 = clipMinX.toInt()
val y0 = clipMinY.toInt()
val x1 = clipMaxX.toInt()
val y1 = clipMaxY.toInt()
val sx = x0.coerceIn(0, fbW)
val sy = y0.coerceIn(0, fbH)
val sw = (x1 - x0).coerceIn(0, fbW - sx)
val sh = (y1 - y0).coerceIn(0, fbH - sy)
if (sw == 0 || sh == 0) continue
scissorOffset.x(sx).y(sy)
scissorExtent.width(sw).height(sh)
vkCmdSetScissor(cmd, 0, scissor)
val firstIndex = globalIdxOffset + dd.getCmdListCmdBufferIdxOffset(listIndex, cmdIndex)
val baseVertex = globalVtxOffset + dd.getCmdListCmdBufferVtxOffset(listIndex, cmdIndex)
vkCmdDrawIndexed(cmd, elemCount, 1, firstIndex, baseVertex, 0)
}
globalVtxOffset += dd.getCmdListVtxBufferSize(listIndex)
globalIdxOffset += dd.getCmdListIdxBufferSize(listIndex)
}
}
private fun updateIo(io: ImGuiIO) {
val nowNs = System.nanoTime()
val dt = ((nowNs - lastFrameNs).coerceAtLeast(1L) / 1e9).toFloat()
lastFrameNs = nowNs
io.setDeltaTime(dt)
flushInputEvents(io)
if (width <= 0 || height <= 0) return
val fbScaleX = displayScale
val fbScaleY = displayScale
// ImGui logical size == RTT size; framebuffer scale is an optional global scale.
io.setDisplaySize(width.toFloat()*fbScaleX, height.toFloat()*fbScaleY)
io.setDisplayFramebufferScale(fbScaleX, fbScaleY)
var skipWindowMouse = false
pointerOverride?.let { override ->
pointerOverride = null
io.addMousePosEvent(override.x / fbScaleX, override.y / fbScaleY)
io.addMouseButtonEvent(0, override.down)
if (override.scrollY != 0f) {
io.addMouseWheelEvent(0f, override.scrollY)
}
skipWindowMouse = true
}
if (useWindowMouseEvents && !skipWindowMouse) {
val w = window ?: return
val cursor = w.cursor
val winW = w.width.coerceAtLeast(1)
val winH = w.height.coerceAtLeast(1)
val u = (cursor.x / winW.toDouble()).coerceIn(0.0, 1.0)
val v = (cursor.y / winH.toDouble()).coerceIn(0.0, 1.0)
io.addMousePosEvent(
(u * width / fbScaleX * 2).toFloat(),
(v * height / fbScaleY * 2).toFloat()
)
io.addMouseButtonEvent(0, w.isMouseButtonPressed(GLFW.GLFW_MOUSE_BUTTON_LEFT))
io.addMouseButtonEvent(1, w.isMouseButtonPressed(GLFW.GLFW_MOUSE_BUTTON_RIGHT))
io.addMouseButtonEvent(2, w.isMouseButtonPressed(GLFW.GLFW_MOUSE_BUTTON_MIDDLE))
val scroll = w.consumeScrollDelta()
if (scroll.x != 0f || scroll.y != 0f) {
io.addMouseWheelEvent(scroll.x, scroll.y)
}
}
}
private fun flushInputEvents(io: ImGuiIO) {
while (pendingKeyEvents.isNotEmpty()) {
val ev = pendingKeyEvents.removeFirst()
if (ev.nativeKey >= 0) {
io.setKeyEventNativeData(ev.imguiKey, ev.nativeKey, ev.nativeScancode)
}
io.addKeyEvent(ev.imguiKey, ev.down)
}
while (pendingChars.isNotEmpty()) {
io.addInputCharacter(pendingChars.removeFirst())
}
}
private fun installInputCallbacks() {
val w = window ?: return
removeKeyListener = w.addKeyListener { key, scancode, action, mods ->
val imguiKey = glfwKeyToImGuiKey(key)
if (imguiKey != ImGuiKey.None) {
val down = action != GLFW.GLFW_RELEASE
pendingKeyEvents.add(KeyEvent(imguiKey, down, key, scancode))
}
updateModKeys(mods)
}
removeCharListener = w.addCharListener { codepoint ->
pendingChars.add(codepoint)
}
}
private fun updateModKeys(mods: Int) {
val ctrl = mods and GLFW.GLFW_MOD_CONTROL != 0
val shift = mods and GLFW.GLFW_MOD_SHIFT != 0
val alt = mods and GLFW.GLFW_MOD_ALT != 0
val sup = mods and GLFW.GLFW_MOD_SUPER != 0
if (ctrl != modCtrl) {
modCtrl = ctrl
pendingKeyEvents.add(KeyEvent(ImGuiKey.ModCtrl, ctrl, -1, -1))
}
if (shift != modShift) {
modShift = shift
pendingKeyEvents.add(KeyEvent(ImGuiKey.ModShift, shift, -1, -1))
}
if (alt != modAlt) {
modAlt = alt
pendingKeyEvents.add(KeyEvent(ImGuiKey.ModAlt, alt, -1, -1))
}
if (sup != modSuper) {
modSuper = sup
pendingKeyEvents.add(KeyEvent(ImGuiKey.ModSuper, sup, -1, -1))
}
}
private fun glfwKeyToImGuiKey(key: Int): Int = when (key) {
GLFW.GLFW_KEY_TAB -> ImGuiKey.Tab
GLFW.GLFW_KEY_LEFT -> ImGuiKey.LeftArrow
GLFW.GLFW_KEY_RIGHT -> ImGuiKey.RightArrow
GLFW.GLFW_KEY_UP -> ImGuiKey.UpArrow
GLFW.GLFW_KEY_DOWN -> ImGuiKey.DownArrow
GLFW.GLFW_KEY_PAGE_UP -> ImGuiKey.PageUp
GLFW.GLFW_KEY_PAGE_DOWN -> ImGuiKey.PageDown
GLFW.GLFW_KEY_HOME -> ImGuiKey.Home
GLFW.GLFW_KEY_END -> ImGuiKey.End
GLFW.GLFW_KEY_INSERT -> ImGuiKey.Insert
GLFW.GLFW_KEY_DELETE -> ImGuiKey.Delete
GLFW.GLFW_KEY_BACKSPACE -> ImGuiKey.Backspace
GLFW.GLFW_KEY_SPACE -> ImGuiKey.Space
GLFW.GLFW_KEY_ENTER -> ImGuiKey.Enter
GLFW.GLFW_KEY_ESCAPE -> ImGuiKey.Escape
GLFW.GLFW_KEY_APOSTROPHE -> ImGuiKey.Apostrophe
GLFW.GLFW_KEY_COMMA -> ImGuiKey.Comma
GLFW.GLFW_KEY_MINUS -> ImGuiKey.Minus
GLFW.GLFW_KEY_PERIOD -> ImGuiKey.Period
GLFW.GLFW_KEY_SLASH -> ImGuiKey.Slash
GLFW.GLFW_KEY_SEMICOLON -> ImGuiKey.Semicolon
GLFW.GLFW_KEY_EQUAL -> ImGuiKey.Equal
GLFW.GLFW_KEY_LEFT_BRACKET -> ImGuiKey.LeftBracket
GLFW.GLFW_KEY_BACKSLASH -> ImGuiKey.Backslash
GLFW.GLFW_KEY_RIGHT_BRACKET -> ImGuiKey.RightBracket
GLFW.GLFW_KEY_GRAVE_ACCENT -> ImGuiKey.GraveAccent
GLFW.GLFW_KEY_CAPS_LOCK -> ImGuiKey.CapsLock
GLFW.GLFW_KEY_SCROLL_LOCK -> ImGuiKey.ScrollLock
GLFW.GLFW_KEY_NUM_LOCK -> ImGuiKey.NumLock
GLFW.GLFW_KEY_PRINT_SCREEN -> ImGuiKey.PrintScreen
GLFW.GLFW_KEY_PAUSE -> ImGuiKey.Pause
GLFW.GLFW_KEY_KP_0 -> ImGuiKey.Keypad0
GLFW.GLFW_KEY_KP_1 -> ImGuiKey.Keypad1
GLFW.GLFW_KEY_KP_2 -> ImGuiKey.Keypad2
GLFW.GLFW_KEY_KP_3 -> ImGuiKey.Keypad3
GLFW.GLFW_KEY_KP_4 -> ImGuiKey.Keypad4
GLFW.GLFW_KEY_KP_5 -> ImGuiKey.Keypad5
GLFW.GLFW_KEY_KP_6 -> ImGuiKey.Keypad6
GLFW.GLFW_KEY_KP_7 -> ImGuiKey.Keypad7
GLFW.GLFW_KEY_KP_8 -> ImGuiKey.Keypad8
GLFW.GLFW_KEY_KP_9 -> ImGuiKey.Keypad9
GLFW.GLFW_KEY_KP_DECIMAL -> ImGuiKey.KeypadDecimal
GLFW.GLFW_KEY_KP_DIVIDE -> ImGuiKey.KeypadDivide
GLFW.GLFW_KEY_KP_MULTIPLY -> ImGuiKey.KeypadMultiply
GLFW.GLFW_KEY_KP_SUBTRACT -> ImGuiKey.KeypadSubtract
GLFW.GLFW_KEY_KP_ADD -> ImGuiKey.KeypadAdd
GLFW.GLFW_KEY_KP_ENTER -> ImGuiKey.KeypadEnter
GLFW.GLFW_KEY_KP_EQUAL -> ImGuiKey.KeypadEqual
GLFW.GLFW_KEY_LEFT_SHIFT -> ImGuiKey.LeftShift
GLFW.GLFW_KEY_LEFT_CONTROL -> ImGuiKey.LeftCtrl
GLFW.GLFW_KEY_LEFT_ALT -> ImGuiKey.LeftAlt
GLFW.GLFW_KEY_LEFT_SUPER -> ImGuiKey.LeftSuper
GLFW.GLFW_KEY_RIGHT_SHIFT -> ImGuiKey.RightShift
GLFW.GLFW_KEY_RIGHT_CONTROL -> ImGuiKey.RightCtrl
GLFW.GLFW_KEY_RIGHT_ALT -> ImGuiKey.RightAlt
GLFW.GLFW_KEY_RIGHT_SUPER -> ImGuiKey.RightSuper
GLFW.GLFW_KEY_MENU -> ImGuiKey.Menu
GLFW.GLFW_KEY_0 -> ImGuiKey._0
GLFW.GLFW_KEY_1 -> ImGuiKey._1
GLFW.GLFW_KEY_2 -> ImGuiKey._2
GLFW.GLFW_KEY_3 -> ImGuiKey._3
GLFW.GLFW_KEY_4 -> ImGuiKey._4
GLFW.GLFW_KEY_5 -> ImGuiKey._5
GLFW.GLFW_KEY_6 -> ImGuiKey._6
GLFW.GLFW_KEY_7 -> ImGuiKey._7
GLFW.GLFW_KEY_8 -> ImGuiKey._8
GLFW.GLFW_KEY_9 -> ImGuiKey._9
GLFW.GLFW_KEY_A -> ImGuiKey.A
GLFW.GLFW_KEY_B -> ImGuiKey.B
GLFW.GLFW_KEY_C -> ImGuiKey.C
GLFW.GLFW_KEY_D -> ImGuiKey.D
GLFW.GLFW_KEY_E -> ImGuiKey.E
GLFW.GLFW_KEY_F -> ImGuiKey.F
GLFW.GLFW_KEY_G -> ImGuiKey.G
GLFW.GLFW_KEY_H -> ImGuiKey.H
GLFW.GLFW_KEY_I -> ImGuiKey.I
GLFW.GLFW_KEY_J -> ImGuiKey.J
GLFW.GLFW_KEY_K -> ImGuiKey.K
GLFW.GLFW_KEY_L -> ImGuiKey.L
GLFW.GLFW_KEY_M -> ImGuiKey.M
GLFW.GLFW_KEY_N -> ImGuiKey.N
GLFW.GLFW_KEY_O -> ImGuiKey.O
GLFW.GLFW_KEY_P -> ImGuiKey.P
GLFW.GLFW_KEY_Q -> ImGuiKey.Q
GLFW.GLFW_KEY_R -> ImGuiKey.R
GLFW.GLFW_KEY_S -> ImGuiKey.S
GLFW.GLFW_KEY_T -> ImGuiKey.T
GLFW.GLFW_KEY_U -> ImGuiKey.U
GLFW.GLFW_KEY_V -> ImGuiKey.V
GLFW.GLFW_KEY_W -> ImGuiKey.W
GLFW.GLFW_KEY_X -> ImGuiKey.X
GLFW.GLFW_KEY_Y -> ImGuiKey.Y
GLFW.GLFW_KEY_Z -> ImGuiKey.Z
GLFW.GLFW_KEY_F1 -> ImGuiKey.F1
GLFW.GLFW_KEY_F2 -> ImGuiKey.F2
GLFW.GLFW_KEY_F3 -> ImGuiKey.F3
GLFW.GLFW_KEY_F4 -> ImGuiKey.F4
GLFW.GLFW_KEY_F5 -> ImGuiKey.F5
GLFW.GLFW_KEY_F6 -> ImGuiKey.F6
GLFW.GLFW_KEY_F7 -> ImGuiKey.F7
GLFW.GLFW_KEY_F8 -> ImGuiKey.F8
GLFW.GLFW_KEY_F9 -> ImGuiKey.F9
GLFW.GLFW_KEY_F10 -> ImGuiKey.F10
GLFW.GLFW_KEY_F11 -> ImGuiKey.F11
GLFW.GLFW_KEY_F12 -> ImGuiKey.F12
GLFW.GLFW_KEY_F13 -> ImGuiKey.F13
GLFW.GLFW_KEY_F14 -> ImGuiKey.F14
GLFW.GLFW_KEY_F15 -> ImGuiKey.F15
GLFW.GLFW_KEY_F16 -> ImGuiKey.F16
GLFW.GLFW_KEY_F17 -> ImGuiKey.F17
GLFW.GLFW_KEY_F18 -> ImGuiKey.F18
GLFW.GLFW_KEY_F19 -> ImGuiKey.F19
GLFW.GLFW_KEY_F20 -> ImGuiKey.F20
GLFW.GLFW_KEY_F21 -> ImGuiKey.F21
GLFW.GLFW_KEY_F22 -> ImGuiKey.F22
GLFW.GLFW_KEY_F23 -> ImGuiKey.F23
GLFW.GLFW_KEY_F24 -> ImGuiKey.F24
else -> ImGuiKey.None
}
private fun estimateContentScale(): Float {
val w = window ?: return 1f
val winW = IntArray(1)
val winH = IntArray(1)
val fbW = IntArray(1)
val fbH = IntArray(1)
GLFW.glfwGetWindowSize(w.windowHandle, winW, winH)
GLFW.glfwGetFramebufferSize(w.windowHandle, fbW, fbH)
val logicalW = winW[0].coerceAtLeast(1).toFloat()
val logicalH = winH[0].coerceAtLeast(1).toFloat()
val scaleX = fbW[0].toFloat() / logicalW
val scaleY = fbH[0].toFloat() / logicalH
return max(1f, max(scaleX, scaleY))
}
fun loadFontFromUrlAsync(url: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit
Download a TTF font asynchronously and use it once ready.
fun loadFontFromUrlAsync(url: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit = {}) {
CoroutineScope(Dispatchers.IO).launch {
try {
val bytes = URI(url).toURL().readBytes()
pendingFont = PendingFont(bytes, sizePixels)
onFinished(true)
} catch (_: Exception) {
onFinished(false)
}
}
}
fun loadFontFromResource(resourcePath: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit
Load a TTF font from application resources (classpath).
fun loadFontFromResource(resourcePath: String, sizePixels: Float = DEFAULT_FONT_SIZE, onFinished: (Boolean) -> Unit = {}) {
try {
val stream = javaClass.classLoader.getResourceAsStream(resourcePath)
?: javaClass.classLoader.getResourceAsStream("/$resourcePath")
?: error("Resource not found: $resourcePath")
stream.use { pendingFont = PendingFont(it.readBytes(), sizePixels) }
onFinished(true)
} catch (_: Exception) {
onFinished(false)
}
}
fun loadFontFromBytes(bytes: ByteArray, sizePixels: Float = DEFAULT_FONT_SIZE)
Load a TTF font from an in-memory byte array immediately (next frame).
body — 59 lines, from line 640
fun loadFontFromBytes(bytes: ByteArray, sizePixels: Float = DEFAULT_FONT_SIZE) {
pendingFont = PendingFont(bytes, sizePixels)
}
private fun rebuildFontTexture(fontData: ByteArray?, sizePixels: Float): StreamingTexture {
val io = ImGui.getIO()
val fonts = io.getFonts()
val fontScale = estimateContentScale()
val superSample = fontSupersample.coerceAtLeast(1f)
io.setFontGlobalScale(1f / (fontScale * superSample))
fonts.clear()
val fontConfig = ImFontConfig()
try {
fontConfig.setSizePixels(sizePixels * fontScale * superSample)
val oversample = superSample.roundToInt().coerceAtLeast(1)
fontConfig.setOversampleH(oversample)
fontConfig.setOversampleV(oversample)
if (fontData != null) {
fonts.addFontFromMemoryTTF(fontData, fontConfig.getSizePixels(), fontConfig)
} else {
fonts.addFontDefault(fontConfig)
}
} finally {
fontConfig.destroy()
}
val w = ImInt()
val h = ImInt()
val rgba = fonts.getTexDataAsRGBA32(w, h)
val width = w.get()
val height = h.get()
val expectedBytes = width * height * 4
val copy = java.nio.ByteBuffer.allocateDirect(expectedBytes).order(ByteOrder.nativeOrder())
val src = rgba.duplicate().order(ByteOrder.nativeOrder())
src.limit(src.position() + expectedBytes)
copy.put(src)
copy.flip()
val newTex = StreamingTexture(
source = copy,
width = width,
height = height,
renderer = renderer,
imageFormat = VK_FORMAT_R8G8B8A8_UNORM,
name = "imgui_font_${width}x$height"
)
newTex.uploadNow(copy)
newTex.cleanupStgBuffer()
fonts.setTexID(fontTexId)
if (this::fontTexture.isInitialized) {
fontTexture.cleanup()
}
fontTexture = newTex
return newTex
}
fun add(name: String, block: ImGuiDsl.(ImGuiIO) -> Unit) {
fun add(name: String, block: ImGuiDsl.(ImGuiIO) -> Unit) {
uiGenerators[name] = generatorFromDsl(block)
}
fun addUi(name: String, block: ImGuiDsl.() -> Unit) {
fun addUi(name: String, block: ImGuiDsl.() -> Unit) {
add(name) { _ -> block() }
}
fun addLegacyUi(name: String, block: (ImGuiIO) -> Unit) {
fun addLegacyUi(name: String, block: (ImGuiIO) -> Unit) {
uiGenerators[name] = generatorFromLegacy(block)
}
fun removeUi(name: String)
fun removeUi(name: String) {
uiGenerators.starts.remove(name)
uiGenerators.ongoings.remove(name)
}
fun injectPointer(x: Float, y: Float, down: Boolean, scrollY: Float = 0f)
Inject a pointer in RTT pixel coordinates (origin top-left). Overrides window mouse for the next frame.
body — 188 lines, from line 721
fun injectPointer(x: Float, y: Float, down: Boolean, scrollY: Float = 0f) {
pointerOverride = PointerOverride(x, y, down, scrollY)
}
private fun applyMonochromeStyle() {
val style = ImGui.getStyle()
val colors: Array<ImVec4> = style.colors
for (c in colors) {
val lum = (0.2126f * c.x + 0.7152f * c.y + 0.0722f * c.z)
c.x = lum
c.y = lum
c.z = lum
}
style.colors = colors
}
private fun applyTightSpacing() {
val style = ImGui.getStyle()
style.windowPadding.set(6f, 6f)
style.framePadding.set(4f, 3f)
style.itemSpacing.set(5f, 4f)
style.itemInnerSpacing.set(4f, 3f)
style.scrollbarSize = 12f
style.indentSpacing = 16f
style.windowRounding = 4f
style.frameRounding = 3f
style.grabRounding = 3f
}
private fun generatorFromLegacy(block: (ImGuiIO) -> Unit): Generator<ImGuiIO, Unit> =
Generator {
var io = yield(Unit)
while (true) {
block(io)
io = yield(Unit)
}
}
private fun generatorFromDsl(block: ImGuiDsl.(ImGuiIO) -> Unit): Generator<ImGuiIO, Unit> =
Generator {
val dsl = ImGuiDsl()
var io = yield(Unit)
while (true) {
dsl.block(io)
io = yield(Unit)
}
}
private fun uploadDrawData(dd: ImDrawData, command: CommandBuffer) {
if (!dd.getValid()) return
val totalVtx = dd.getTotalVtxCount()
val totalIdx = dd.getTotalIdxCount()
if (totalVtx <= 0 || totalIdx <= 0) return
ensureCapacity(totalVtx, totalIdx)
val pos = requireNotNull(posBuffer)
val uv = requireNotNull(uvBuffer)
val col = requireNotNull(colorBuffer)
val idx = requireNotNull(indexBuffer)
pos.trimTo(totalVtx * 8L)
uv.trimTo(totalVtx * 8L)
col.trimTo(totalVtx * 4L)
idx.trimTo(totalIdx * 4L)
val vtxStride = ImDrawData.sizeOfImDrawVert()
val idxStride = ImDrawData.sizeOfImDrawIdx()
require(vtxStride >= 20) { "Unexpected ImDrawVert stride: $vtxStride" }
require(idxStride == 2 || idxStride == 4) { "Unexpected ImDrawIdx stride: $idxStride" }
// Write staging buffers (src) in one pass.
val posPtr = pos.src.map()
val uvPtr = uv.src.map()
val colPtr = col.src.map()
val idxPtr = idx.src.map()
try {
val posFb = MemoryUtil.memFloatBuffer(posPtr, (pos.src.requestedSize / 4L).toInt())
val uvFb = MemoryUtil.memFloatBuffer(uvPtr, (uv.src.requestedSize / 4L).toInt())
val colIb = MemoryUtil.memIntBuffer(colPtr, (col.src.requestedSize / 4L).toInt())
val idxIb = MemoryUtil.memIntBuffer(idxPtr, (idx.src.requestedSize / 4L).toInt())
posFb.clear()
uvFb.clear()
colIb.clear()
idxIb.clear()
val cmdLists = dd.getCmdListsCount()
for (listIndex in 0 until cmdLists) {
val vtxCount = dd.getCmdListVtxBufferSize(listIndex)
val idxCount = dd.getCmdListIdxBufferSize(listIndex)
val vtxBuf = dd.getCmdListVtxBufferData(listIndex).order(ByteOrder.nativeOrder())
for (i in 0 until vtxCount) {
val base = i * vtxStride
val x = vtxBuf.getFloat(base + 0)
val y = vtxBuf.getFloat(base + 4)
val u = vtxBuf.getFloat(base + 8)
val v = vtxBuf.getFloat(base + 12)
val c = vtxBuf.getInt(base + 16)
posFb.put(x).put(y)
uvFb.put(u).put(v)
colIb.put(c)
}
val idxBuf = dd.getCmdListIdxBufferData(listIndex).order(ByteOrder.nativeOrder())
if (idxStride == 2) {
for (i in 0 until idxCount) {
val v = idxBuf.getShort(i * 2).toInt() and 0xFFFF
idxIb.put(v)
}
} else {
for (i in 0 until idxCount) {
idxIb.put(idxBuf.getInt(i * 4))
}
}
}
} finally {
pos.src.unmap(0, pos.neededSize)
uv.src.unmap(0, uv.neededSize)
col.src.unmap(0, col.neededSize)
idx.src.unmap(0, idx.neededSize)
pos.watermark++
uv.watermark++
col.watermark++
idx.watermark++
}
// Enqueue copies into the device-local buffers.
pos.clean(hostReading = false, cmd = command)
uv.clean(hostReading = false, cmd = command)
col.clean(hostReading = false, cmd = command)
idx.clean(hostReading = false, cmd = command)
// Ensure the transfer writes are visible to subsequent vertex input fetches in this same command buffer.
MemoryStack.stackPush().use { stack ->
val dstAccess = VK_ACCESS_2_VERTEX_ATTRIBUTE_READ_BIT or VK_ACCESS_2_INDEX_READ_BIT
val barriers = VkBufferMemoryBarrier2.calloc(4, stack)
fun fill(i: Int, buf: VertexBuffer, size: Long) {
barriers[i]
.sType(VK_STRUCTURE_TYPE_BUFFER_MEMORY_BARRIER_2)
.srcStageMask(VK_PIPELINE_STAGE_2_ALL_TRANSFER_BIT)
.srcAccessMask(VK_ACCESS_2_TRANSFER_WRITE_BIT)
.dstStageMask(VK_PIPELINE_STAGE_2_VERTEX_INPUT_BIT)
.dstAccessMask(dstAccess)
.srcQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.dstQueueFamilyIndex(VK_QUEUE_FAMILY_IGNORED)
.buffer(buf.dest.buffer)
.offset(0L)
.size(size)
}
fill(0, pos, pos.neededSize)
fill(1, uv, uv.neededSize)
fill(2, col, col.neededSize)
fill(3, idx, idx.neededSize)
val dep = VkDependencyInfo.calloc(stack)
.sType(VK_STRUCTURE_TYPE_DEPENDENCY_INFO)
.pBufferMemoryBarriers(barriers)
vkCmdPipelineBarrier2(command.vkCommandBuffer, dep)
}
}
private fun ensureCapacity(vertexCount: Int, indexCount: Int) {
val wantV = max(1024, vertexCount)
val wantI = max(2048, indexCount)
if (posBuffer != null && vertexCapacity >= wantV && indexCapacity >= wantI) return
val nextV = max(wantV, (vertexCapacity * 1.5f).toInt())
val nextI = max(wantI, (indexCapacity * 1.5f).toInt())
posBuffer?.cleanup()
uvBuffer?.cleanup()
colorBuffer?.cleanup()
indexBuffer?.cleanup()
posBuffer = VertexBuffer(renderer, nextV * 8L, usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
uvBuffer = VertexBuffer(renderer, nextV * 8L, usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
colorBuffer = VertexBuffer(renderer, nextV * 4L, usage = VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
indexBuffer = VertexBuffer(renderer, nextI * 4L, usage = VK_BUFFER_USAGE_INDEX_BUFFER_BIT)
vertexCapacity = nextV
indexCapacity = nextI
}
class ImGuiDsl
class ImGuiDsl {
private val instance = ImGui()
fun window(title: String, flags: Int = 0, block: ImGui.() -> Unit) {
fun window(title: String, flags: Int = 0, block: ImGui.() -> Unit) {
if (ImGui.begin(title, flags)) {
with(instance) { block() }
}
ImGui.end()
}
fun gui(block: ImGui.() -> Unit) {
fun gui(block: ImGui.() -> Unit) {
with(instance) { block() }
}
}
private data class KeyEvent(val imguiKey: Int, val down: Boolean, val nativeKey: Int, val nativeScancode: Int)
private data class PendingFont(val bytes: ByteArray, val sizePixels: Float)
private data class PointerOverride(val x: Float, val y: Float, val down: Boolean, val scrollY: Float)
companion object
companion object {
private const val DEFAULT_FONT_SIZE = 13f
private const val DEFAULT_NAME = "imgui-rtt"
private val instances = WeakHashMap<Renderer, MutableMap<String, ImGuiRenderToTexture>>()
private val imguiVertexShader = """
#version 460
layout(location = 0) in vec2 inPosition;
layout(location = 1) in vec2 inUV;
layout(location = 2) in uint inColor;
layout(location = 0) out vec2 fragUV;
layout(location = 1) out vec4 fragColor;
layout(push_constant) uniform Push {
vec2 scale;
vec2 translate;
} push;
void main() {
fragUV = inUV;
fragColor = unpackUnorm4x8(inColor);
gl_Position = vec4(inPosition * push.scale + push.translate, 0.0, 1.0);
}
""".trimIndent()
private val imguiFragmentShader = """
#version 460
layout(location = 0) in vec2 fragUV;
layout(location = 1) in vec4 fragColor;
layout(location = 0) out vec4 outColor;
layout(set = 0, binding = 0) uniform sampler2D fontTexture;
void main() {
outColor = fragColor * texture(fontTexture, fragUV);
}
""".trimIndent()
fun get
Return a per-Renderer singleton (keyed by name) created on demand.
@JvmStatic
fun get(
renderer: Renderer = fromContext(),
window: Window = renderer.window,
name: String = DEFAULT_NAME,
overSample: Int = 2,
fontSuperSample: Float = 0f,
target: RenderTarget = renderer.renderList
): ImGuiRenderToTexture {
val byName = instances.getOrPut(renderer) { mutableMapOf() }
return byName.getOrPut(name) {
create(renderer, window, name, overSample, fontSuperSample, target)
}
}
operator fun invoke
body — 43 lines, from line 988
operator fun invoke(
renderer: Renderer = fromContext(),
window: Window = renderer.window,
name: String = DEFAULT_NAME,
overSample: Int = 2,
fontSuperSample: Float = 4f,
target: RenderTarget = renderer.renderList
): ImGuiRenderToTexture = get(renderer, window, name, overSample, fontSuperSample, target)
private fun create(
renderer: Renderer,
window: Window,
name: String,
overSample: Int,
fontSuperSample: Float,
target: RenderTarget
): ImGuiRenderToTexture {
val imgui = ImGuiRenderToTexture(
renderer = renderer,
window = window,
parameters = {
width = window.width * overSample
height = window.height * overSample
mipLevels = 4
generateColorMipmaps = true
},
fontSupersample = fontSuperSample,
displayScale = 1f // overSample.f
)
imgui.addUi("test") {
window("ImGui → RenderToTexture") {
text("Hello from imgui-java")
text("Renderer: Vulkan → RTT → screen blit")
}
}
imgui.loadFontFromResource("IosevkaField-Light.ttf", sizePixels = 12 * 2 * overSample.f)
imgui.attachTo(target = target, name = name, blitName = "$name-blit")
return imgui
}
}
}