Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 14 additions & 8 deletions src/figdraw/common/textbackends/common.nim
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,11 @@ proc calcMinMaxContent*(
var curr: Slice[int]
var currLen: float
var maxWidth: float
var rect: Rect = rect(float32.high, float32.high, 0, 0)
var
minX = float32.high
minY = float32.high
maxX = -float32.high
maxY = -float32.high

let glyphCount =
if textLayout.arrangedGlyphs.len > 0:
Expand All @@ -39,10 +43,10 @@ proc calcMinMaxContent*(
textLayout.runes[idx]

maxWidth += glyphRect.w
rect.x = min(rect.x, glyphRect.x)
rect.y = min(rect.y, glyphRect.y)
rect.w = max(rect.w, glyphRect.x + glyphRect.w)
rect.h = max(rect.h, glyphRect.y + glyphRect.h)
minX = min(minX, glyphRect.x)
minY = min(minY, glyphRect.y)
maxX = max(maxX, glyphRect.x + glyphRect.w)
maxY = max(maxY, glyphRect.y + glyphRect.h)

if glyphRune.isWhiteSpace:
curr = idx + 1 .. idx
Expand Down Expand Up @@ -71,9 +75,11 @@ proc calcMinMaxContent*(
result.maxSize.x = maxWidth
result.maxSize.y = wordsHeight

if glyphCount == 0:
rect = rect(0, 0, 0, 0)
result.bounding = rect
result.bounding =
if glyphCount == 0:
rect(0, 0, 0, 0)
else:
rect(minX, minY, maxX - minX, maxY - minY)

proc maxFontSize*(fontSizes: openArray[float]): float32 =
for size in fontSizes:
Expand Down
11 changes: 9 additions & 2 deletions src/figdraw/figrender.nim
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import std/[hashes, math, os, strutils, tables, unicode]
export tables

from pkg/pixie import Image
import pkg/pixie
import pkg/chroma
import pkg/chronicles

Expand Down Expand Up @@ -183,6 +183,13 @@ proc takeScreenshot*[BackendState](
): Image =
renderer.ctx.readPixels(frame, readFront = readFront)

proc takeOneFrameScreenshot*[BackendState](
renderer: FigRenderer[BackendState], frame: Rect = rect(0, 0, 0, 0)
): Image =
## Captures the frame that was just rendered by renderFrame().
## OpenGL draws into the back buffer until the windowing layer swaps.
renderer.takeScreenshot(frame, readFront = renderer.backendKind() != rbOpenGL)

proc logBackend(msg: static string) =
info msg, preferredBackend = backendName(PreferredBackendKind)

Expand Down Expand Up @@ -943,6 +950,6 @@ proc renderFrame*[BackendState](
when defined(testOneFrame) and (UseOpenGlBackend or UseOpenGlFallback):
## This is used for test only
## Take a screen shot of the first frame and exit.
var img = takeScreenshot(renderer)
var img = takeOneFrameScreenshot(renderer)
img.writeFile("screenshot.png")
quit()
50 changes: 34 additions & 16 deletions src/figdraw/vulkan/vulkan_context.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1985,22 +1985,40 @@ proc flush(ctx: VulkanContext) =
v.sdfPad = 0'u16
v.sdfFactors[0] = ctx.sdfFactors[i * 2 + 0]
v.sdfFactors[1] = ctx.sdfFactors[i * 2 + 1]
v.rectMaskParams[0] = ctx.rectMaskParams[i * 4 + 0]
v.rectMaskParams[1] = ctx.rectMaskParams[i * 4 + 1]
v.rectMaskParams[2] = ctx.rectMaskParams[i * 4 + 2]
v.rectMaskParams[3] = ctx.rectMaskParams[i * 4 + 3]
v.rectMaskRadii[0] = ctx.rectMaskRadii[i * 4 + 0]
v.rectMaskRadii[1] = ctx.rectMaskRadii[i * 4 + 1]
v.rectMaskRadii[2] = ctx.rectMaskRadii[i * 4 + 2]
v.rectMaskRadii[3] = ctx.rectMaskRadii[i * 4 + 3]
v.rectMaskMatX[0] = ctx.rectMaskMatX[i * 4 + 0]
v.rectMaskMatX[1] = ctx.rectMaskMatX[i * 4 + 1]
v.rectMaskMatX[2] = ctx.rectMaskMatX[i * 4 + 2]
v.rectMaskMatX[3] = ctx.rectMaskMatX[i * 4 + 3]
v.rectMaskMatY[0] = ctx.rectMaskMatY[i * 4 + 0]
v.rectMaskMatY[1] = ctx.rectMaskMatY[i * 4 + 1]
v.rectMaskMatY[2] = ctx.rectMaskMatY[i * 4 + 2]
v.rectMaskMatY[3] = ctx.rectMaskMatY[i * 4 + 3]
if ctx.batchHasRectMask:
v.rectMaskParams[0] = ctx.rectMaskParams[i * 4 + 0]
v.rectMaskParams[1] = ctx.rectMaskParams[i * 4 + 1]
v.rectMaskParams[2] = ctx.rectMaskParams[i * 4 + 2]
v.rectMaskParams[3] = ctx.rectMaskParams[i * 4 + 3]
v.rectMaskRadii[0] = ctx.rectMaskRadii[i * 4 + 0]
v.rectMaskRadii[1] = ctx.rectMaskRadii[i * 4 + 1]
v.rectMaskRadii[2] = ctx.rectMaskRadii[i * 4 + 2]
v.rectMaskRadii[3] = ctx.rectMaskRadii[i * 4 + 3]
v.rectMaskMatX[0] = ctx.rectMaskMatX[i * 4 + 0]
v.rectMaskMatX[1] = ctx.rectMaskMatX[i * 4 + 1]
v.rectMaskMatX[2] = ctx.rectMaskMatX[i * 4 + 2]
v.rectMaskMatX[3] = ctx.rectMaskMatX[i * 4 + 3]
v.rectMaskMatY[0] = ctx.rectMaskMatY[i * 4 + 0]
v.rectMaskMatY[1] = ctx.rectMaskMatY[i * 4 + 1]
v.rectMaskMatY[2] = ctx.rectMaskMatY[i * 4 + 2]
v.rectMaskMatY[3] = ctx.rectMaskMatY[i * 4 + 3]
else:
v.rectMaskParams[0] = 0.0'f32
v.rectMaskParams[1] = 0.0'f32
v.rectMaskParams[2] = -1.0'f32
v.rectMaskParams[3] = -1.0'f32
v.rectMaskRadii[0] = 0.0'f32
v.rectMaskRadii[1] = 0.0'f32
v.rectMaskRadii[2] = 0.0'f32
v.rectMaskRadii[3] = 0.0'f32
v.rectMaskMatX[0] = 0.0'f32
v.rectMaskMatX[1] = 0.0'f32
v.rectMaskMatX[2] = 0.0'f32
v.rectMaskMatX[3] = 0.0'f32
v.rectMaskMatY[0] = 0.0'f32
v.rectMaskMatY[1] = 0.0'f32
v.rectMaskMatY[2] = 0.0'f32
v.rectMaskMatY[3] = 0.0'f32

let uploadBytes = VkDeviceSize(vertexCount * sizeof(Vertex))
let vertexAlloc = ctx.createBuffer(
Expand Down
36 changes: 0 additions & 36 deletions src/figdraw/windowing/siwinshim.nim
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import std/[os, strutils]
when defined(linux) or defined(bsd):
import std/osproc
import vmath

import siwin/[window as siWindow, windowOpengl as siWindowOpengl]
Expand Down Expand Up @@ -39,36 +37,6 @@ proc siwinBackendName*[BackendState](renderer: FigRenderer[BackendState]): strin

var siwinGlobalsShared {.threadvar.}: SiwinGlobals

when defined(linux) or defined(bsd):
var cachedXftScale {.threadvar.}: float32
var cachedXftScaleInitialized {.threadvar.}: bool

proc xftDpiScale(): float32 =
## Xft.dpi gives a useful fractional scale for X11 and many XWayland sessions.
## Cache once per thread to avoid shelling out repeatedly.
if cachedXftScaleInitialized:
return cachedXftScale
cachedXftScaleInitialized = true
cachedXftScale = 0.0
if getEnv("DISPLAY").len == 0:
return cachedXftScale
try:
let output = execProcess("xrdb -query")
for line in output.splitLines():
let trimmed = line.strip()
if not trimmed.toLowerAscii().startsWith("xft.dpi:"):
continue
let parts = trimmed.split(":", maxsplit = 1)
if parts.len != 2:
break
let dpi = parts[1].strip().parseFloat()
if dpi > 0:
cachedXftScale = (dpi / 96.0).float32
break
except CatchableError:
discard
cachedXftScale

proc sharedSiwinGlobals*(): SiwinGlobals =
if siwinGlobalsShared.isNil:
siwinGlobalsShared = newSiwinGlobals()
Expand Down Expand Up @@ -283,10 +251,6 @@ proc contentScale*(window: Window): float32 =
elif defined(linux) or defined(bsd):
let backendScale = window.uiScale()
let scale = if backendScale > 0: backendScale else: 1.0
let xftScale = xftDpiScale()
if xftScale > 0:
if window of siX11Window.WindowX11:
return xftScale
scale
else:
1.0
Expand Down
8 changes: 4 additions & 4 deletions tests/opengl_test_utils.nim
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,11 @@ proc renderAndScreenshotOnce*(
renderer.renderFrame(renders, sz)
if renderer.backendKind() == rbOpenGL:
# OpenGL fallback renders into the back buffer; capture before swap.
result = glrenderer.takeScreenshot(renderer, readFront = false)
result = glrenderer.takeOneFrameScreenshot(renderer)
renderer.endFrame()
else:
renderer.endFrame()
result = glrenderer.takeScreenshot(renderer)
result = glrenderer.takeOneFrameScreenshot(renderer)
result.writeFile(outputPath)
except VulkanError as exc:
raise newException(WindyError, "Vulkan device not available: " & exc.msg)
Expand All @@ -87,7 +87,7 @@ proc renderAndScreenshotOnce*(
var renders = makeRenders(sz.x, sz.y)
renderer.renderFrame(renders, sz)
glFinish()
result = glrenderer.takeScreenshot(renderer, readFront = false)
result = glrenderer.takeOneFrameScreenshot(renderer)
window.swapBuffers()
result.writeFile(outputPath)
finally:
Expand Down Expand Up @@ -126,7 +126,7 @@ proc renderAndScreenshotOverlayOnce*(
renders, vec2(windowW.float32, windowH.float32), clearMain = true
)
glFinish()
result = glrenderer.takeScreenshot(renderer, readFront = false)
result = glrenderer.takeOneFrameScreenshot(renderer)
window.swapBuffers()
result.writeFile(outputPath)
finally:
Expand Down
104 changes: 98 additions & 6 deletions tests/siwin_test_utils.nim
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import std/[os, strutils]
import std/os
import pkg/pixie

import figdraw/windowing/siwinshim
Expand Down Expand Up @@ -55,11 +55,11 @@ proc renderAndScreenshotOnce*(
renderer.beginFrame()
renderer.renderFrame(renders, sz)
if renderer.backendKind() == rbOpenGL:
result = glrenderer.takeScreenshot(renderer, readFront = false)
result = glrenderer.takeOneFrameScreenshot(renderer)
renderer.endFrame()
else:
renderer.endFrame()
result = glrenderer.takeScreenshot(renderer)
result = glrenderer.takeOneFrameScreenshot(renderer)
if result.isNil or result.width <= 0 or result.height <= 0 or result.data.len == 0:
raise newException(
ValueError, "Vulkan screenshot unavailable (no present target or empty frame)"
Expand All @@ -81,13 +81,105 @@ proc renderAndScreenshotOnce*(
let sz = window.logicalSize()
var renders = makeRenders(sz.x, sz.y)
let renderer = glrenderer.newFigRenderer(atlasSize = atlasSize)
renderer.beginFrame()
renderer.renderFrame(renders, sz)
glFinish()
result = glrenderer.takeScreenshot(renderer, readFront = false)
renderer.endFrame()
result = glrenderer.takeOneFrameScreenshot(renderer)
presentNow(window)
result.writeFile(outputPath)
finally:
when not defined(emscripten):
window.close()

proc renderAndScreenshotSequence*(
makeInitialRenders: proc(w, h: float32): Renders {.closure.},
makeUpdatedRenders: proc(w, h: float32): Renders {.closure.},
initialPath: string,
updatedPath: string,
windowW = 800,
windowH = 600,
atlasSize = 2048,
title = "figdraw test: siwin screenshot sequence",
): tuple[initial, updated: Image] =
when UseMetalBackend:
try:
let renderer = glrenderer.newFigRenderer(atlasSize = atlasSize)

var initialRenders = makeInitialRenders(windowW.float32, windowH.float32)
renderer.renderFrame(initialRenders, vec2(windowW.float32, windowH.float32))
result.initial = glrenderer.takeScreenshot(renderer)
result.initial.writeFile(initialPath)

var updatedRenders = makeUpdatedRenders(windowW.float32, windowH.float32)
renderer.renderFrame(updatedRenders, vec2(windowW.float32, windowH.float32))
result.updated = glrenderer.takeScreenshot(renderer)
result.updated.writeFile(updatedPath)
except ValueError:
raise newException(ValueError, "Metal device not available")
elif UseVulkanBackend:
let renderer = glrenderer.newFigRenderer(
atlasSize = atlasSize, backendState = SiwinRenderBackend()
)
let window = newSiwinWindow(
renderer,
size = ivec2(windowW.int32, windowH.int32),
fullscreen = false,
title = title,
)

proc capture(
makeRenders: proc(w, h: float32): Renders {.closure.}, outputPath: string
): Image =
let sz = window.logicalSize()
var renders = makeRenders(sz.x, sz.y)
renderer.beginFrame()
renderer.renderFrame(renders, sz)
if renderer.backendKind() == rbOpenGL:
result = glrenderer.takeOneFrameScreenshot(renderer)
renderer.endFrame()
else:
renderer.endFrame()
result = glrenderer.takeOneFrameScreenshot(renderer)
if result.isNil or result.width <= 0 or result.height <= 0 or result.data.len == 0:
raise newException(
ValueError, "Vulkan screenshot unavailable (no present target or empty frame)"
)
result.writeFile(outputPath)

try:
renderer.setupBackend(window)
window.firstStep()
result.initial = capture(makeInitialRenders, initialPath)
result.updated = capture(makeUpdatedRenders, updatedPath)
except VulkanError as exc:
raise newException(ValueError, "Vulkan device not available: " & exc.msg)
except ValueError:
raise newException(ValueError, "Vulkan device not available")
finally:
when not defined(emscripten):
window.close()
else:
let window = newSiwinWindow(
size = ivec2(windowW.int32, windowH.int32), fullscreen = false, title = title
)
try:
window.firstStep()
let
sz = window.logicalSize()
renderer = glrenderer.newFigRenderer(atlasSize = atlasSize)

var initialRenders = makeInitialRenders(sz.x, sz.y)
renderer.renderFrame(initialRenders, sz)
glFinish()
result.initial = glrenderer.takeOneFrameScreenshot(renderer)
presentNow(window)
result.initial.writeFile(initialPath)

var updatedRenders = makeUpdatedRenders(sz.x, sz.y)
renderer.renderFrame(updatedRenders, sz)
glFinish()
result.updated = glrenderer.takeOneFrameScreenshot(renderer)
presentNow(window)
result.updated.writeFile(updatedPath)
finally:
when not defined(emscripten):
window.close()
Loading
Loading