Skip to content

Instantly share code, notes, and snippets.

@DoctorGester
Created March 19, 2026 10:01
Show Gist options
  • Select an option

  • Save DoctorGester/88ee4992f45b3992e55566b824fbc333 to your computer and use it in GitHub Desktop.

Select an option

Save DoctorGester/88ee4992f45b3992e55566b824fbc333 to your computer and use it in GitHub Desktop.
#import <AppKit/AppKit.h>
#import <Metal/Metal.h>
#import <MetalKit/MetalKit.h>
#import <QuartzCore/QuartzCore.h>
#include <ft2build.h>
#include FT_FREETYPE_H
#include FT_OUTLINE_H
#define KB_TEXT_SHAPE_IMPLEMENTATION
#include "../vendor/kb/kb_text_shape.h"
#include <algorithm>
#include <array>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <filesystem>
#include <iomanip>
#include <iostream>
#include <limits>
#include <map>
#include <memory>
#include <sstream>
#include <simd/simd.h>
#include <stdexcept>
#include <string>
#include <string_view>
#include <vector>
namespace slug
{
namespace fs = std::filesystem;
constexpr uint32_t kParagraphWidth = 900;
constexpr uint32_t kFontSizePixels = 48;
constexpr float kParagraphPadding = 48.0f;
constexpr float kBandDilation = 0.0f;
struct CurveData
{
float p1[2];
float p2[2];
float p3[2];
float minX;
float minY;
float maxX;
float maxY;
};
struct LiveCurveData
{
float p1[2];
__fp16 a[2];
__fp16 b[2];
__fp16 minX;
__fp16 minY;
__fp16 maxX;
__fp16 maxY;
};
struct BandHeader
{
uint32_t count;
uint32_t offset;
uint32_t negOffset;
float splitCoord;
};
struct RenderConfig
{
float glyphMinX = 0.0f;
float glyphMinY = 0.0f;
float glyphMaxX = 0.0f;
float glyphMaxY = 0.0f;
float hScale = 1.0f;
float hOffset = 0.0f;
float vScale = 1.0f;
float vOffset = 0.0f;
uint32_t hBandCount = 1;
uint32_t vBandCount = 1;
};
struct QuadraticCurve
{
std::array<float, 2> p1 {};
std::array<float, 2> p2 {};
std::array<float, 2> p3 {};
float minX = 0.0f;
float minY = 0.0f;
float maxX = 0.0f;
float maxY = 0.0f;
};
struct PolygonVertex
{
float position[2];
float normal[2];
};
struct GlyphPreparation
{
std::vector<CurveData> curves;
std::vector<BandHeader> hHeaders;
std::vector<uint32_t> hIndices;
std::vector<BandHeader> vHeaders;
std::vector<uint32_t> vIndices;
std::vector<PolygonVertex> polygonVertices;
RenderConfig config {};
};
struct FontResource
{
size_t index = 0;
fs::path path;
FT_Face face = nullptr;
kbts_shape_context *shapeContext = nullptr;
kbts_font *shapeFont = nullptr;
kbts_font_info2_1 fontInfo {};
float pixelsPerUnit = 1.0f;
float ascentPx = 0.0f;
float descentPx = 0.0f;
float lineGapPx = 0.0f;
};
struct StyledSpan
{
size_t fontIndex = 0;
std::string text;
};
struct TextToken
{
size_t fontIndex = 0;
std::string text;
bool whitespace = false;
bool newline = false;
};
struct GlyphPlacement
{
size_t fontIndex = 0;
uint32_t glyphIndex = 0;
float offsetX = 0.0f;
float offsetY = 0.0f;
};
struct ShapedToken
{
size_t fontIndex = 0;
std::string text;
bool whitespace = false;
bool newline = false;
float advanceX = 0.0f;
std::vector<GlyphPlacement> glyphs;
};
struct PositionedGlyph
{
size_t fontIndex = 0;
uint32_t glyphIndex = 0;
float originX = 0.0f;
float originY = 0.0f;
};
struct ParagraphLayout
{
float contentWidth = 0.0f;
float canvasWidth = 0.0f;
float canvasHeight = 0.0f;
float lineAdvance = 0.0f;
float maxAscent = 0.0f;
float maxDescent = 0.0f;
std::vector<PositionedGlyph> glyphs;
uint32_t lineCount = 0;
};
struct GlyphKey
{
size_t fontIndex = 0;
uint32_t glyphIndex = 0;
bool operator<(const GlyphKey &other) const
{
if (fontIndex != other.fontIndex)
{
return fontIndex < other.fontIndex;
}
return glyphIndex < other.glyphIndex;
}
};
struct PageVertex
{
simd_float3 position;
simd_float2 texCoord;
};
struct LiveGlyphVertex
{
simd_float4 posNorm;
simd_float2 sampleCoord;
simd_float4 jacobian;
simd_float4 banding;
simd_uint4 glyph;
};
struct SceneUniforms
{
simd_float4 row0;
simd_float4 row1;
simd_float4 row2;
simd_float4 row3;
simd_float2 viewport;
simd_float2 pad;
simd_float4 inkColor;
simd_float4 paperColor;
};
struct LiveFrameGlyphInfo
{
GlyphPreparation *prep = nullptr;
uint32_t hHeaderBase = 0;
uint32_t vHeaderBase = 0;
};
struct LiveFrame
{
std::vector<PageVertex> pageVertices;
std::vector<LiveGlyphVertex> glyphVertices;
std::vector<LiveCurveData> curves;
std::vector<BandHeader> hHeaders;
std::vector<uint32_t> hIndices;
std::vector<BandHeader> vHeaders;
std::vector<uint32_t> vIndices;
float pageAspect = 1.0f;
double shapeMs = 0.0;
double prepMs = 0.0;
size_t glyphCount = 0;
size_t uniqueGlyphCount = 0;
};
struct CameraState
{
float yaw = 0.0f;
float pitch = 0.18f;
float distance = 3.6f;
};
struct HudFrame
{
std::vector<LiveGlyphVertex> glyphVertices;
std::vector<LiveCurveData> curves;
std::vector<BandHeader> hHeaders;
std::vector<uint32_t> hIndices;
std::vector<BandHeader> vHeaders;
std::vector<uint32_t> vIndices;
};
struct HudMetrics
{
double fps = 0.0;
double cpuFrameMs = 0.0;
double gpuFrameMs = 0.0;
double sceneShapeMs = 0.0;
double scenePrepMs = 0.0;
size_t glyphCount = 0;
size_t uniqueGlyphCount = 0;
};
struct AppRuntime
{
id<MTLDevice> device = nil;
std::unique_ptr<class LiveParagraphRenderer> liveRenderer;
std::unique_ptr<class HudTextRenderer> hudRenderer;
};
static constexpr const char *kShaderSource = R"METAL(
#include <metal_stdlib>
using namespace metal;
struct PageVertexIn
{
float3 position [[attribute(0)]];
float2 texCoord [[attribute(1)]];
};
struct GlyphVertexIn
{
float4 posNorm [[attribute(0)]];
float2 sampleCoord [[attribute(1)]];
float4 jacobian [[attribute(2)]];
float4 banding [[attribute(3)]];
uint4 glyph [[attribute(4)]];
};
struct LiveCurveData
{
float2 p1;
half2 a;
half2 b;
half minX;
half minY;
half maxX;
half maxY;
};
struct BandHeader
{
uint count;
uint offset;
uint negOffset;
float splitCoord;
};
struct SceneUniforms
{
float4 row0;
float4 row1;
float4 row2;
float4 row3;
float2 viewport;
float2 pad;
float4 inkColor;
float4 paperColor;
};
struct PageVertexOut
{
float4 position [[position]];
float2 texCoord;
};
struct GlyphVertexOut
{
float4 position [[position]];
float2 renderCoord;
float4 banding [[flat]];
uint4 glyph [[flat]];
};
static inline float saturate1(float value)
{
return clamp(value, 0.0f, 1.0f);
}
static inline uint calcRootCode(float y1, float y2, float y3)
{
uint i1 = as_type<uint>(y1) >> 31u;
uint i2 = as_type<uint>(y2) >> 30u;
uint i3 = as_type<uint>(y3) >> 29u;
uint shift = (i2 & 2u) | (i1 & ~2u);
shift = (i3 & 4u) | (shift & ~4u);
return ((0x2E74u >> shift) & 0x0101u);
}
static inline float2 solveHorizPoly(float2 p1, float2 a, float2 b)
{
float ra = 1.0f / a.y;
float rb = 0.5f / b.y;
float d = sqrt(max(b.y * b.y - a.y * p1.y, 0.0f));
float t1 = (b.y - d) * ra;
float t2 = (b.y + d) * ra;
if (fabs(a.y) < (1.0f / 65536.0f))
{
t1 = p1.y * rb;
t2 = t1;
}
return float2((a.x * t1 - b.x * 2.0f) * t1 + p1.x,
(a.x * t2 - b.x * 2.0f) * t2 + p1.x);
}
static inline float2 solveVertPoly(float2 p1, float2 a, float2 b)
{
float ra = 1.0f / a.x;
float rb = 0.5f / b.x;
float d = sqrt(max(b.x * b.x - a.x * p1.x, 0.0f));
float t1 = (b.x - d) * ra;
float t2 = (b.x + d) * ra;
if (fabs(a.x) < (1.0f / 65536.0f))
{
t1 = p1.x * rb;
t2 = t1;
}
return float2((a.y * t1 - b.y * 2.0f) * t1 + p1.y,
(a.y * t2 - b.y * 2.0f) * t2 + p1.y);
}
static inline float calcCoverage(float xcov, float ycov, float xwgt, float ywgt)
{
float blended = fabs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, 1.0f / 65536.0f);
return saturate1(max(blended, min(fabs(xcov), fabs(ycov))));
}
static inline float positiveCoverage(float root)
{
return saturate1(root + 0.5f);
}
static inline float2 slugDilate(float4 posNorm,
float2 sampleCoord,
float4 jacobian,
constant SceneUniforms &uniforms,
thread float2 &vpos)
{
float2 normal = normalize(posNorm.zw);
float s = dot(uniforms.row3.xy, posNorm.xy) + uniforms.row3.w;
float t = dot(uniforms.row3.xy, normal);
float u = (s * dot(uniforms.row0.xy, normal) - t * (dot(uniforms.row0.xy, posNorm.xy) + uniforms.row0.w)) * uniforms.viewport.x;
float v = (s * dot(uniforms.row1.xy, normal) - t * (dot(uniforms.row1.xy, posNorm.xy) + uniforms.row1.w)) * uniforms.viewport.y;
float s2 = s * s;
float st = s * t;
float uv = u * u + v * v;
float2 delta = posNorm.zw * (s2 * (st + sqrt(max(uv, 0.0f))) / max(uv - st * st, 1.0e-6f));
vpos = posNorm.xy + delta;
return float2(sampleCoord.x + dot(delta, jacobian.xy),
sampleCoord.y + dot(delta, jacobian.zw));
}
static inline float4 clipPosition(float3 point, constant SceneUniforms &uniforms)
{
return float4(dot(uniforms.row0.xyz, point) + uniforms.row0.w,
dot(uniforms.row1.xyz, point) + uniforms.row1.w,
dot(uniforms.row2.xyz, point) + uniforms.row2.w,
dot(uniforms.row3.xyz, point) + uniforms.row3.w);
}
vertex PageVertexOut page_vertex(PageVertexIn in [[stage_in]],
constant SceneUniforms &uniforms [[buffer(1)]])
{
PageVertexOut out;
out.position = clipPosition(in.position, uniforms);
out.texCoord = in.texCoord;
return out;
}
fragment float4 page_background_fragment(PageVertexOut in [[stage_in]],
constant SceneUniforms &uniforms [[buffer(1)]])
{
(void)in;
return uniforms.paperColor;
}
vertex GlyphVertexOut slug_live_vertex(GlyphVertexIn in [[stage_in]],
constant SceneUniforms &uniforms [[buffer(1)]])
{
GlyphVertexOut out;
float2 dilatedPosition;
out.renderCoord = slugDilate(in.posNorm, in.sampleCoord, in.jacobian, uniforms, dilatedPosition);
out.position = clipPosition(float3(dilatedPosition, 0.0f), uniforms);
out.banding = in.banding;
out.glyph = in.glyph;
return out;
}
fragment float4 slug_live_fragment(GlyphVertexOut in [[stage_in]],
constant SceneUniforms &uniforms [[buffer(1)]],
const device LiveCurveData *curves [[buffer(2)]],
const device BandHeader *hHeaders [[buffer(3)]],
const device uint *hIndices [[buffer(4)]],
const device BandHeader *vHeaders [[buffer(5)]],
const device uint *vIndices [[buffer(6)]])
{
float2 emsPerPixel = fwidth(in.renderCoord);
float2 pixelsPerEm = 1.0f / max(emsPerPixel, float2(1.0e-5f));
float2 earlyOutThreshold = in.renderCoord - emsPerPixel * 0.5f;
int hBandCount = max(int(in.glyph.z), 1);
int vBandCount = max(int(in.glyph.w), 1);
int hBand = clamp(int(in.renderCoord.y * in.banding.y + in.banding.w), 0, hBandCount - 1);
int vBand = clamp(int(in.renderCoord.x * in.banding.x + in.banding.z), 0, vBandCount - 1);
float xcov = 0.0f;
float xwgt = 0.0f;
BandHeader hHeader = hHeaders[in.glyph.x + uint(hBand)];
bool hNegative = in.renderCoord.x < hHeader.splitCoord;
uint hOffset = hNegative ? hHeader.negOffset : hHeader.offset;
for (uint i = 0; i < hHeader.count; ++i)
{
LiveCurveData curve = curves[hIndices[hOffset + i]];
float curveMinX = float(curve.minX);
float curveMaxX = float(curve.maxX);
if ((!hNegative && curveMaxX < earlyOutThreshold.x) ||
(hNegative && curveMinX > (in.renderCoord.x + emsPerPixel.x * 0.5f)))
{
break;
}
float2 a = float2(curve.a);
float2 b = float2(curve.b);
float2 p1 = float2(curve.p1) - in.renderCoord;
float y2 = p1.y - b.y;
float y3 = a.y + p1.y - b.y * 2.0f;
uint code = calcRootCode(p1.y, y2, y3);
if (code != 0u)
{
float2 roots = solveHorizPoly(p1, a, b) * pixelsPerEm.x;
if ((code & 1u) != 0u)
{
xcov += hNegative ? -saturate1(0.5f - roots.x) : positiveCoverage(roots.x);
xwgt = max(xwgt, saturate1(1.0f - fabs(roots.x) * 2.0f));
}
if (code > 1u)
{
xcov += hNegative ? saturate1(0.5f - roots.y) : -positiveCoverage(roots.y);
xwgt = max(xwgt, saturate1(1.0f - fabs(roots.y) * 2.0f));
}
}
}
float ycov = 0.0f;
float ywgt = 0.0f;
BandHeader vHeader = vHeaders[in.glyph.y + uint(vBand)];
bool vNegative = in.renderCoord.y < vHeader.splitCoord;
uint vOffset = vNegative ? vHeader.negOffset : vHeader.offset;
for (uint i = 0; i < vHeader.count; ++i)
{
LiveCurveData curve = curves[vIndices[vOffset + i]];
float curveMinY = float(curve.minY);
float curveMaxY = float(curve.maxY);
if ((!vNegative && curveMaxY < earlyOutThreshold.y) ||
(vNegative && curveMinY > (in.renderCoord.y + emsPerPixel.y * 0.5f)))
{
break;
}
float2 a = float2(curve.a);
float2 b = float2(curve.b);
float2 p1 = float2(curve.p1) - in.renderCoord;
float x2 = p1.x - b.x;
float x3 = a.x + p1.x - b.x * 2.0f;
uint code = calcRootCode(p1.x, x2, x3);
if (code != 0u)
{
float2 roots = solveVertPoly(p1, a, b) * pixelsPerEm.y;
if ((code & 1u) != 0u)
{
ycov += vNegative ? saturate1(0.5f - roots.x) : -positiveCoverage(roots.x);
ywgt = max(ywgt, saturate1(1.0f - fabs(roots.x) * 2.0f));
}
if (code > 1u)
{
ycov += vNegative ? -saturate1(0.5f - roots.y) : positiveCoverage(roots.y);
ywgt = max(ywgt, saturate1(1.0f - fabs(roots.y) * 2.0f));
}
}
}
float coverage = calcCoverage(xcov, ycov, xwgt, ywgt);
return float4(uniforms.inkColor.rgb, coverage * uniforms.inkColor.a);
}
)METAL";
template <typename T>
static id<MTLBuffer> createSharedBuffer(id<MTLDevice> device, const std::vector<T> &items)
{
size_t bytes = items.size() * sizeof(T);
if (bytes == 0)
{
return [device newBufferWithLength:sizeof(T) options:MTLResourceStorageModeShared];
}
return [device newBufferWithBytes:items.data() length:bytes options:MTLResourceStorageModeShared];
}
template <typename T>
static id<MTLBuffer> createPrivateBuffer(id<MTLDevice> device,
id<MTLCommandQueue> queue,
const std::vector<T> &items)
{
const uint8_t zero = 0;
const void *bytes = items.empty() ? static_cast<const void *>(&zero) : static_cast<const void *>(items.data());
size_t length = std::max<size_t>(items.size() * sizeof(T), 1);
id<MTLBuffer> staging = [device newBufferWithBytes:bytes length:length options:MTLResourceStorageModeShared];
id<MTLBuffer> buffer = [device newBufferWithLength:length options:MTLResourceStorageModePrivate];
id<MTLCommandBuffer> commandBuffer = [queue commandBuffer];
id<MTLBlitCommandEncoder> blit = [commandBuffer blitCommandEncoder];
[blit copyFromBuffer:staging sourceOffset:0 toBuffer:buffer destinationOffset:0 size:length];
[blit endEncoding];
[commandBuffer commit];
[commandBuffer waitUntilCompleted];
return buffer;
}
static simd_float4x4 MatrixPerspective(float fovyRadians, float aspect, float nearZ, float farZ)
{
float yScale = 1.0f / std::tan(fovyRadians * 0.5f);
float xScale = yScale / aspect;
float zRange = farZ - nearZ;
float zScale = -(farZ + nearZ) / zRange;
float wzScale = -2.0f * farZ * nearZ / zRange;
return simd_matrix(
simd_make_float4(xScale, 0.0f, 0.0f, 0.0f),
simd_make_float4(0.0f, yScale, 0.0f, 0.0f),
simd_make_float4(0.0f, 0.0f, zScale, -1.0f),
simd_make_float4(0.0f, 0.0f, wzScale, 0.0f));
}
static simd_float4x4 MatrixLookAt(simd_float3 eye, simd_float3 target, simd_float3 up)
{
simd_float3 zAxis = simd_normalize(eye - target);
simd_float3 xAxis = simd_normalize(simd_cross(up, zAxis));
simd_float3 yAxis = simd_cross(zAxis, xAxis);
return simd_matrix(
simd_make_float4(xAxis.x, yAxis.x, zAxis.x, 0.0f),
simd_make_float4(xAxis.y, yAxis.y, zAxis.y, 0.0f),
simd_make_float4(xAxis.z, yAxis.z, zAxis.z, 0.0f),
simd_make_float4(-simd_dot(xAxis, eye), -simd_dot(yAxis, eye), -simd_dot(zAxis, eye), 1.0f));
}
static fs::path locateProjectRoot()
{
fs::path path = fs::current_path();
while (true)
{
if (fs::exists(path / "vendor" / "kb" / "kb_text_shape.h"))
{
return path;
}
if (path == path.root_path())
{
break;
}
path = path.parent_path();
}
throw std::runtime_error("Could not locate project root containing vendor/kb/kb_text_shape.h");
}
static std::vector<fs::path> discoverFonts(const fs::path &root)
{
std::vector<fs::path> fonts;
for (const auto &entry : fs::directory_iterator(root))
{
if (entry.is_regular_file() && entry.path().extension() == ".ttf")
{
fonts.push_back(entry.path());
}
}
std::sort(fonts.begin(), fonts.end());
return fonts;
}
static void destroyFonts(std::vector<FontResource> &fonts)
{
for (auto &font : fonts)
{
if (font.shapeContext != nullptr)
{
kbts_DestroyShapeContext(font.shapeContext);
font.shapeContext = nullptr;
}
if (font.face != nullptr)
{
FT_Done_Face(font.face);
font.face = nullptr;
}
}
}
static std::vector<FontResource> loadFonts(FT_Library library, const std::vector<fs::path> &fontPaths, uint32_t sizePixels)
{
std::vector<FontResource> fonts;
fonts.reserve(fontPaths.size());
try
{
for (size_t index = 0; index < fontPaths.size(); ++index)
{
FT_Face face {};
if (FT_New_Face(library, fontPaths[index].c_str(), 0, &face) != 0)
{
throw std::runtime_error("Failed to load FreeType face: " + fontPaths[index].filename().string());
}
if (FT_Set_Pixel_Sizes(face, 0, sizePixels) != 0)
{
FT_Done_Face(face);
throw std::runtime_error("Failed to size face: " + fontPaths[index].filename().string());
}
kbts_shape_context *shapeContext = kbts_CreateShapeContext(nullptr, nullptr);
if (shapeContext == nullptr)
{
FT_Done_Face(face);
throw std::runtime_error("Failed to create kb_text_shape context");
}
kbts_font *shapeFont = kbts_ShapePushFontFromFile(shapeContext, fontPaths[index].c_str(), 0);
if (shapeFont == nullptr)
{
kbts_DestroyShapeContext(shapeContext);
FT_Done_Face(face);
throw std::runtime_error("Failed to parse shaping font: " + fontPaths[index].filename().string());
}
FontResource resource;
resource.index = index;
resource.path = fontPaths[index];
resource.face = face;
resource.shapeContext = shapeContext;
resource.shapeFont = shapeFont;
resource.fontInfo.Base.Size = sizeof(resource.fontInfo);
kbts_GetFontInfo2(shapeFont, &resource.fontInfo.Base);
float unitsPerEm = std::max(1, int(resource.fontInfo.UnitsPerEm));
resource.pixelsPerUnit = static_cast<float>(sizePixels) / unitsPerEm;
resource.ascentPx = resource.fontInfo.Ascent * resource.pixelsPerUnit;
resource.descentPx = -resource.fontInfo.Descent * resource.pixelsPerUnit;
resource.lineGapPx = resource.fontInfo.LineGap * resource.pixelsPerUnit;
fonts.push_back(resource);
}
}
catch (...)
{
destroyFonts(fonts);
throw;
}
return fonts;
}
static std::vector<StyledSpan> buildParagraphSpans(const std::vector<FontResource> &fonts)
{
if (fonts.empty())
{
return {};
}
auto pick = [&](size_t index) {
return index % fonts.size();
};
return {
{ pick(0), "Slug now renders a real paragraph directly on the GPU, " },
{ pick(1), "with kb_text_shape handling the shaped runs, " },
{ pick(2), "the cached quadratic outlines feeding the live fragment path, " },
{ pick(3), "and the whole page orbiting as a single camera-facing surface.\n" },
{ pick(0), "Drag to rotate, scroll to zoom, and use the minimal single-file renderer as a starting point for further experiments." }
};
}
static std::vector<TextToken> tokenizeSpans(const std::vector<StyledSpan> &spans)
{
std::vector<TextToken> tokens;
for (const auto &span : spans)
{
size_t index = 0;
while (index < span.text.size())
{
unsigned char ch = static_cast<unsigned char>(span.text[index]);
if (ch == '\n')
{
tokens.push_back(TextToken { span.fontIndex, "\n", false, true });
++index;
continue;
}
bool whitespace = std::isspace(ch) != 0;
size_t start = index;
while (index < span.text.size())
{
unsigned char current = static_cast<unsigned char>(span.text[index]);
if (current == '\n')
{
break;
}
if ((std::isspace(current) != 0) != whitespace)
{
break;
}
++index;
}
tokens.push_back(TextToken {
span.fontIndex,
span.text.substr(start, index - start),
whitespace,
false
});
}
}
return tokens;
}
static ShapedToken shapeToken(const FontResource &font, const TextToken &token)
{
ShapedToken shaped;
shaped.fontIndex = token.fontIndex;
shaped.text = token.text;
shaped.whitespace = token.whitespace;
shaped.newline = token.newline;
if (token.newline)
{
return shaped;
}
kbts_ShapeBegin(font.shapeContext, KBTS_DIRECTION_DONT_KNOW, KBTS_LANGUAGE_DONT_KNOW);
kbts_ShapeUtf8(font.shapeContext, token.text.c_str(), static_cast<int>(token.text.size()), KBTS_USER_ID_GENERATION_MODE_CODEPOINT_INDEX);
kbts_ShapeEnd(font.shapeContext);
if (kbts_ShapeError(font.shapeContext) != KBTS_SHAPE_ERROR_NONE)
{
throw std::runtime_error("kb_text_shape failed on token: " + token.text);
}
int cursorX = 0;
int cursorY = 0;
kbts_run run {};
while (kbts_ShapeRun(font.shapeContext, &run))
{
kbts_glyph *glyph = nullptr;
while (kbts_GlyphIteratorNext(&run.Glyphs, &glyph))
{
shaped.glyphs.push_back(GlyphPlacement {
font.index,
glyph->Id,
(cursorX + glyph->OffsetX) * font.pixelsPerUnit,
(cursorY + glyph->OffsetY) * font.pixelsPerUnit
});
cursorX += glyph->AdvanceX;
cursorY += glyph->AdvanceY;
}
}
shaped.advanceX = cursorX * font.pixelsPerUnit;
return shaped;
}
static ParagraphLayout layoutParagraph(const std::vector<FontResource> &fonts,
const std::vector<ShapedToken> &tokens,
uint32_t paragraphWidth)
{
ParagraphLayout layout;
layout.contentWidth = static_cast<float>(paragraphWidth);
for (const auto &font : fonts)
{
layout.maxAscent = std::max(layout.maxAscent, font.ascentPx);
layout.maxDescent = std::max(layout.maxDescent, font.descentPx);
layout.lineAdvance = std::max(layout.lineAdvance, font.ascentPx + font.descentPx + font.lineGapPx);
}
layout.lineAdvance = std::max(layout.lineAdvance, layout.maxAscent + layout.maxDescent + 6.0f);
layout.canvasWidth = layout.contentWidth + kParagraphPadding * 2.0f;
float cursorX = kParagraphPadding;
float baselineY = kParagraphPadding + layout.maxAscent;
layout.lineCount = 1;
for (const auto &token : tokens)
{
if (token.newline)
{
cursorX = kParagraphPadding;
baselineY += layout.lineAdvance;
++layout.lineCount;
continue;
}
if (token.whitespace && std::abs(cursorX - kParagraphPadding) < 0.01f)
{
continue;
}
float lineLimit = kParagraphPadding + layout.contentWidth;
if (!token.whitespace && cursorX > kParagraphPadding && cursorX + token.advanceX > lineLimit)
{
cursorX = kParagraphPadding;
baselineY += layout.lineAdvance;
++layout.lineCount;
}
for (const auto &glyph : token.glyphs)
{
layout.glyphs.push_back(PositionedGlyph {
glyph.fontIndex,
glyph.glyphIndex,
cursorX + glyph.offsetX,
baselineY - glyph.offsetY
});
}
cursorX += token.advanceX;
}
layout.canvasHeight = baselineY + layout.maxDescent + kParagraphPadding;
return layout;
}
static std::array<float, 2> toPoint(FT_Vector point)
{
return { static_cast<float>(point.x) / 64.0f, static_cast<float>(point.y) / 64.0f };
}
static QuadraticCurve makeQuadratic(const std::array<float, 2> &p1,
const std::array<float, 2> &p2,
const std::array<float, 2> &p3)
{
QuadraticCurve curve;
curve.p1 = p1;
curve.p2 = p2;
curve.p3 = p3;
curve.minX = std::min({ p1[0], p2[0], p3[0] });
curve.minY = std::min({ p1[1], p2[1], p3[1] });
curve.maxX = std::max({ p1[0], p2[0], p3[0] });
curve.maxY = std::max({ p1[1], p2[1], p3[1] });
return curve;
}
class OutlineCollector
{
public:
explicit OutlineCollector(std::vector<QuadraticCurve> &curves) : curves_(curves) {}
static int moveTo(const FT_Vector *to, void *user)
{
auto *self = static_cast<OutlineCollector *>(user);
self->current_ = toPoint(*to);
return 0;
}
static int lineTo(const FT_Vector *to, void *user)
{
auto *self = static_cast<OutlineCollector *>(user);
std::array<float, 2> target = toPoint(*to);
std::array<float, 2> control = {
(self->current_[0] + target[0]) * 0.5f,
(self->current_[1] + target[1]) * 0.5f
};
self->curves_.push_back(makeQuadratic(self->current_, control, target));
self->current_ = target;
return 0;
}
static int conicTo(const FT_Vector *control, const FT_Vector *to, void *user)
{
auto *self = static_cast<OutlineCollector *>(user);
std::array<float, 2> q = toPoint(*control);
std::array<float, 2> target = toPoint(*to);
self->curves_.push_back(makeQuadratic(self->current_, q, target));
self->current_ = target;
return 0;
}
static int cubicTo(const FT_Vector *control1, const FT_Vector *control2, const FT_Vector *to, void *user)
{
auto *self = static_cast<OutlineCollector *>(user);
std::array<float, 2> p0 = self->current_;
std::array<float, 2> p1 = toPoint(*control1);
std::array<float, 2> p2 = toPoint(*control2);
std::array<float, 2> p3 = toPoint(*to);
constexpr int subdivisions = 8;
std::array<float, 2> last = p0;
for (int i = 1; i <= subdivisions; ++i)
{
float t = static_cast<float>(i) / static_cast<float>(subdivisions);
float omt = 1.0f - t;
std::array<float, 2> point = {
omt * omt * omt * p0[0] + 3.0f * omt * omt * t * p1[0] + 3.0f * omt * t * t * p2[0] + t * t * t * p3[0],
omt * omt * omt * p0[1] + 3.0f * omt * omt * t * p1[1] + 3.0f * omt * t * t * p2[1] + t * t * t * p3[1]
};
std::array<float, 2> control = { (last[0] + point[0]) * 0.5f, (last[1] + point[1]) * 0.5f };
self->curves_.push_back(makeQuadratic(last, control, point));
last = point;
}
self->current_ = p3;
return 0;
}
private:
std::vector<QuadraticCurve> &curves_;
std::array<float, 2> current_ { 0.0f, 0.0f };
};
struct CornerClip
{
bool enabled = false;
float edgeX = 0.0f;
float edgeY = 0.0f;
float area = 0.0f;
};
static CornerClip chooseCornerClip(float minX,
float minY,
float maxX,
float maxY,
const std::vector<std::array<float, 2>> &controlPoints,
int signX,
int signY)
{
static constexpr float kCandidateNormals[][2] = {
{ 1.0f, 1.0f },
{ 2.0f, 1.0f },
{ 1.0f, 2.0f },
{ 3.0f, 1.0f },
{ 1.0f, 3.0f },
};
constexpr float kMinClipArea = 0.5f;
constexpr float kEpsilon = 1.0e-4f;
CornerClip best;
float cornerX = (signX > 0) ? maxX : minX;
float cornerY = (signY > 0) ? maxY : minY;
for (const auto &candidate : kCandidateNormals)
{
float nx = candidate[0] * static_cast<float>(signX);
float ny = candidate[1] * static_cast<float>(signY);
float support = -std::numeric_limits<float>::infinity();
for (const auto &point : controlPoints)
{
support = std::max(support, nx * point[0] + ny * point[1]);
}
float cornerSupport = nx * cornerX + ny * cornerY;
if (support >= cornerSupport - kEpsilon)
{
continue;
}
float xOnHorizontal = (support - ny * cornerY) / nx;
float yOnVertical = (support - nx * cornerX) / ny;
if (!(xOnHorizontal > minX + kEpsilon && xOnHorizontal < maxX - kEpsilon &&
yOnVertical > minY + kEpsilon && yOnVertical < maxY - kEpsilon))
{
continue;
}
bool inwardX = (signX > 0) ? (xOnHorizontal < cornerX - kEpsilon) : (xOnHorizontal > cornerX + kEpsilon);
bool inwardY = (signY > 0) ? (yOnVertical < cornerY - kEpsilon) : (yOnVertical > cornerY + kEpsilon);
if (!inwardX || !inwardY)
{
continue;
}
float area = 0.5f * std::abs(cornerX - xOnHorizontal) * std::abs(cornerY - yOnVertical);
if (area >= kMinClipArea && area > best.area)
{
best.enabled = true;
best.edgeX = xOnHorizontal;
best.edgeY = yOnVertical;
best.area = area;
}
}
return best;
}
static std::vector<PolygonVertex> makeRectanglePolygon(float minX, float minY, float maxX, float maxY)
{
return {
{ { minX, minY }, { -1.0f, -1.0f } },
{ { maxX, minY }, { 1.0f, -1.0f } },
{ { maxX, maxY }, { 1.0f, 1.0f } },
{ { minX, maxY }, { -1.0f, 1.0f } },
};
}
static std::vector<PolygonVertex> buildBoundingPolygon(float minX,
float minY,
float maxX,
float maxY,
const std::vector<std::array<float, 2>> &controlPoints)
{
constexpr float kEpsilon = 1.0e-4f;
if (controlPoints.empty())
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
CornerClip clips[4] = {
chooseCornerClip(minX, minY, maxX, maxY, controlPoints, -1, -1),
chooseCornerClip(minX, minY, maxX, maxY, controlPoints, 1, -1),
chooseCornerClip(minX, minY, maxX, maxY, controlPoints, 1, 1),
chooseCornerClip(minX, minY, maxX, maxY, controlPoints, -1, 1),
};
auto disableSmallerClip = [&](int lhs, int rhs) {
if (!clips[lhs].enabled || !clips[rhs].enabled)
{
return;
}
if (clips[lhs].area <= clips[rhs].area)
{
clips[lhs].enabled = false;
}
else
{
clips[rhs].enabled = false;
}
};
if (clips[0].enabled && clips[1].enabled && !(clips[0].edgeX < clips[1].edgeX - kEpsilon))
{
disableSmallerClip(0, 1);
}
if (clips[3].enabled && clips[2].enabled && !(clips[3].edgeX < clips[2].edgeX - kEpsilon))
{
disableSmallerClip(3, 2);
}
if (clips[0].enabled && clips[3].enabled && !(clips[0].edgeY < clips[3].edgeY - kEpsilon))
{
disableSmallerClip(0, 3);
}
if (clips[1].enabled && clips[2].enabled && !(clips[1].edgeY < clips[2].edgeY - kEpsilon))
{
disableSmallerClip(1, 2);
}
struct Point2
{
float x;
float y;
};
std::vector<Point2> points;
points.reserve(8);
auto appendPoint = [&](float x, float y) {
if (!points.empty())
{
const Point2 &last = points.back();
if (std::abs(last.x - x) < kEpsilon && std::abs(last.y - y) < kEpsilon)
{
return;
}
}
points.push_back(Point2 { x, y });
};
appendPoint(clips[0].enabled ? clips[0].edgeX : minX, minY);
appendPoint(clips[1].enabled ? clips[1].edgeX : maxX, minY);
if (clips[1].enabled)
{
appendPoint(maxX, clips[1].edgeY);
}
appendPoint(maxX, clips[2].enabled ? clips[2].edgeY : maxY);
if (clips[2].enabled)
{
appendPoint(clips[2].edgeX, maxY);
}
appendPoint(clips[3].enabled ? clips[3].edgeX : minX, maxY);
if (clips[3].enabled)
{
appendPoint(minX, clips[3].edgeY);
}
if (clips[0].enabled)
{
appendPoint(minX, clips[0].edgeY);
}
if (points.size() < 3)
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
float signedArea = 0.0f;
for (size_t i = 0; i < points.size(); ++i)
{
const Point2 &a = points[i];
const Point2 &b = points[(i + 1) % points.size()];
signedArea += a.x * b.y - b.x * a.y;
}
if (std::abs(signedArea) < kEpsilon)
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
if (signedArea < 0.0f)
{
std::reverse(points.begin(), points.end());
}
for (size_t i = 0; i < points.size(); ++i)
{
const Point2 &a = points[i];
const Point2 &b = points[(i + 1) % points.size()];
const Point2 &c = points[(i + 2) % points.size()];
float cross = (b.x - a.x) * (c.y - b.y) - (b.y - a.y) * (c.x - b.x);
if (cross <= kEpsilon)
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
}
std::vector<std::array<float, 2>> edgeNormals(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
const Point2 &a = points[i];
const Point2 &b = points[(i + 1) % points.size()];
float edgeX = b.x - a.x;
float edgeY = b.y - a.y;
float length = std::sqrt(edgeX * edgeX + edgeY * edgeY);
if (length < kEpsilon)
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
edgeNormals[i] = { edgeY / length, -edgeX / length };
}
std::vector<PolygonVertex> polygon;
polygon.reserve(points.size());
for (size_t i = 0; i < points.size(); ++i)
{
const auto &prevNormal = edgeNormals[(i + points.size() - 1) % points.size()];
const auto &nextNormal = edgeNormals[i];
float det = prevNormal[0] * nextNormal[1] - prevNormal[1] * nextNormal[0];
if (det <= kEpsilon)
{
return makeRectanglePolygon(minX, minY, maxX, maxY);
}
float normalX = (nextNormal[1] - prevNormal[1]) / det;
float normalY = (prevNormal[0] - nextNormal[0]) / det;
polygon.push_back(PolygonVertex {
{ points[i].x, points[i].y },
{ normalX, normalY }
});
}
return polygon;
}
static GlyphPreparation prepareGlyph(FT_Face face, uint32_t glyphIndex, uint32_t sizePixels)
{
if (FT_Set_Pixel_Sizes(face, 0, sizePixels) != 0)
{
throw std::runtime_error("FT_Set_Pixel_Sizes failed");
}
if (FT_Load_Glyph(face, glyphIndex, FT_LOAD_NO_HINTING | FT_LOAD_NO_BITMAP) != 0)
{
throw std::runtime_error("FT_Load_Glyph failed");
}
GlyphPreparation prep;
FT_BBox bbox {};
if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE && face->glyph->outline.n_points > 0)
{
FT_Outline_Get_CBox(&face->glyph->outline, &bbox);
}
prep.config.glyphMinX = static_cast<float>(bbox.xMin) / 64.0f;
prep.config.glyphMinY = static_cast<float>(bbox.yMin) / 64.0f;
prep.config.glyphMaxX = static_cast<float>(bbox.xMax) / 64.0f;
prep.config.glyphMaxY = static_cast<float>(bbox.yMax) / 64.0f;
std::vector<QuadraticCurve> curves;
if (face->glyph->format == FT_GLYPH_FORMAT_OUTLINE && face->glyph->outline.n_points > 0)
{
OutlineCollector collector(curves);
FT_Outline_Funcs funcs {};
funcs.move_to = &OutlineCollector::moveTo;
funcs.line_to = &OutlineCollector::lineTo;
funcs.conic_to = &OutlineCollector::conicTo;
funcs.cubic_to = &OutlineCollector::cubicTo;
if (FT_Outline_Decompose(&face->glyph->outline, &funcs, &collector) != 0)
{
throw std::runtime_error("FT_Outline_Decompose failed");
}
}
std::vector<std::array<float, 2>> controlPoints;
controlPoints.reserve(curves.size() * 3);
prep.curves.reserve(curves.size());
for (const auto &curve : curves)
{
CurveData data {};
data.p1[0] = curve.p1[0];
data.p1[1] = curve.p1[1];
data.p2[0] = curve.p2[0];
data.p2[1] = curve.p2[1];
data.p3[0] = curve.p3[0];
data.p3[1] = curve.p3[1];
data.minX = curve.minX;
data.minY = curve.minY;
data.maxX = curve.maxX;
data.maxY = curve.maxY;
prep.curves.push_back(data);
controlPoints.push_back(curve.p1);
controlPoints.push_back(curve.p2);
controlPoints.push_back(curve.p3);
}
prep.polygonVertices = buildBoundingPolygon(prep.config.glyphMinX,
prep.config.glyphMinY,
prep.config.glyphMaxX,
prep.config.glyphMaxY,
controlPoints);
float width = std::max(prep.config.glyphMaxX - prep.config.glyphMinX, 1.0f);
float height = std::max(prep.config.glyphMaxY - prep.config.glyphMinY, 1.0f);
auto chooseBandCount = [](float dimension) {
uint32_t rounded = std::max(1u, static_cast<uint32_t>(std::ceil(dimension)));
if (rounded < 24u)
{
return std::max(1u, rounded / 2u);
}
return 24u;
};
prep.config.hBandCount = chooseBandCount(height);
prep.config.vBandCount = chooseBandCount(width);
prep.config.hScale = static_cast<float>(prep.config.hBandCount) / height;
prep.config.hOffset = -prep.config.glyphMinY * prep.config.hScale;
prep.config.vScale = static_cast<float>(prep.config.vBandCount) / width;
prep.config.vOffset = -prep.config.glyphMinX * prep.config.vScale;
std::vector<std::vector<uint32_t>> hBands(prep.config.hBandCount);
std::vector<std::vector<uint32_t>> vBands(prep.config.vBandCount);
for (uint32_t index = 0; index < curves.size(); ++index)
{
const auto &curve = curves[index];
int hStart = std::clamp(static_cast<int>(std::floor((curve.minY - kBandDilation) * prep.config.hScale + prep.config.hOffset)),
0,
static_cast<int>(prep.config.hBandCount) - 1);
int hEnd = std::clamp(static_cast<int>(std::floor((curve.maxY + kBandDilation) * prep.config.hScale + prep.config.hOffset)),
0,
static_cast<int>(prep.config.hBandCount) - 1);
for (int band = hStart; band <= hEnd; ++band)
{
hBands[static_cast<size_t>(band)].push_back(index);
}
int vStart = std::clamp(static_cast<int>(std::floor((curve.minX - kBandDilation) * prep.config.vScale + prep.config.vOffset)),
0,
static_cast<int>(prep.config.vBandCount) - 1);
int vEnd = std::clamp(static_cast<int>(std::floor((curve.maxX + kBandDilation) * prep.config.vScale + prep.config.vOffset)),
0,
static_cast<int>(prep.config.vBandCount) - 1);
for (int band = vStart; band <= vEnd; ++band)
{
vBands[static_cast<size_t>(band)].push_back(index);
}
}
prep.hHeaders.reserve(hBands.size());
for (auto &band : hBands)
{
std::sort(band.begin(), band.end(), [&](uint32_t lhs, uint32_t rhs) {
return curves[lhs].maxX > curves[rhs].maxX;
});
prep.hHeaders.push_back(BandHeader { static_cast<uint32_t>(band.size()), static_cast<uint32_t>(prep.hIndices.size()), 0u, 0.0f });
prep.hIndices.insert(prep.hIndices.end(), band.begin(), band.end());
}
prep.vHeaders.reserve(vBands.size());
for (auto &band : vBands)
{
std::sort(band.begin(), band.end(), [&](uint32_t lhs, uint32_t rhs) {
return curves[lhs].maxY > curves[rhs].maxY;
});
prep.vHeaders.push_back(BandHeader { static_cast<uint32_t>(band.size()), static_cast<uint32_t>(prep.vIndices.size()), 0u, 0.0f });
prep.vIndices.insert(prep.vIndices.end(), band.begin(), band.end());
}
return prep;
}
static LiveFrameGlyphInfo appendFrameGlyphData(const GlyphPreparation &prep,
std::vector<LiveCurveData> &curves,
std::vector<BandHeader> &hHeaders,
std::vector<uint32_t> &hIndices,
std::vector<BandHeader> &vHeaders,
std::vector<uint32_t> &vIndices)
{
LiveFrameGlyphInfo info;
info.prep = const_cast<GlyphPreparation *>(&prep);
info.hHeaderBase = static_cast<uint32_t>(hHeaders.size());
info.vHeaderBase = static_cast<uint32_t>(vHeaders.size());
uint32_t curveBase = static_cast<uint32_t>(curves.size());
curves.reserve(curves.size() + prep.curves.size());
for (const CurveData &curve : prep.curves)
{
LiveCurveData live {};
live.p1[0] = curve.p1[0];
live.p1[1] = curve.p1[1];
live.a[0] = static_cast<__fp16>(curve.p1[0] - curve.p2[0] * 2.0f + curve.p3[0]);
live.a[1] = static_cast<__fp16>(curve.p1[1] - curve.p2[1] * 2.0f + curve.p3[1]);
live.b[0] = static_cast<__fp16>(curve.p1[0] - curve.p2[0]);
live.b[1] = static_cast<__fp16>(curve.p1[1] - curve.p2[1]);
live.minX = static_cast<__fp16>(curve.minX);
live.minY = static_cast<__fp16>(curve.minY);
live.maxX = static_cast<__fp16>(curve.maxX);
live.maxY = static_cast<__fp16>(curve.maxY);
curves.push_back(live);
}
auto bestSplitCoord = [&](const std::vector<uint32_t> &indices, bool horizontal) {
if (indices.empty())
{
return 0.0f;
}
std::vector<float> mins;
std::vector<float> maxs;
mins.reserve(indices.size());
maxs.reserve(indices.size());
for (uint32_t index : indices)
{
const LiveCurveData &curve = curves[curveBase + index];
mins.push_back(horizontal ? curve.minX : curve.minY);
maxs.push_back(horizontal ? curve.maxX : curve.maxY);
}
std::sort(mins.begin(), mins.end());
std::sort(maxs.begin(), maxs.end());
size_t minCursor = 0;
size_t maxCursor = 0;
uint32_t negCount = 0;
uint32_t posCount = static_cast<uint32_t>(indices.size());
uint32_t bestCost = posCount;
float bestCoord = (mins.front() + maxs.back()) * 0.5f;
std::vector<float> events;
events.reserve(mins.size() + maxs.size());
events.insert(events.end(), mins.begin(), mins.end());
events.insert(events.end(), maxs.begin(), maxs.end());
std::sort(events.begin(), events.end());
for (float coord : events)
{
while (minCursor < mins.size() && mins[minCursor] <= coord)
{
++negCount;
++minCursor;
}
while (maxCursor < maxs.size() && maxs[maxCursor] < coord)
{
--posCount;
++maxCursor;
}
uint32_t cost = std::max(negCount, posCount);
if (cost < bestCost)
{
bestCost = cost;
bestCoord = coord;
}
}
return bestCoord;
};
for (const BandHeader &header : prep.hHeaders)
{
std::vector<uint32_t> positive;
positive.reserve(header.count);
for (uint32_t i = 0; i < header.count; ++i)
{
positive.push_back(prep.hIndices[header.offset + i]);
}
std::vector<uint32_t> negative = positive;
std::sort(negative.begin(), negative.end(), [&](uint32_t lhs, uint32_t rhs) {
return curves[curveBase + lhs].minX < curves[curveBase + rhs].minX;
});
float splitCoord = bestSplitCoord(positive, true);
uint32_t posOffset = static_cast<uint32_t>(hIndices.size());
for (uint32_t index : positive)
{
hIndices.push_back(curveBase + index);
}
uint32_t negOffset = static_cast<uint32_t>(hIndices.size());
for (uint32_t index : negative)
{
hIndices.push_back(curveBase + index);
}
hHeaders.push_back(BandHeader { header.count, posOffset, negOffset, splitCoord });
}
for (const BandHeader &header : prep.vHeaders)
{
std::vector<uint32_t> positive;
positive.reserve(header.count);
for (uint32_t i = 0; i < header.count; ++i)
{
positive.push_back(prep.vIndices[header.offset + i]);
}
std::vector<uint32_t> negative = positive;
std::sort(negative.begin(), negative.end(), [&](uint32_t lhs, uint32_t rhs) {
return curves[curveBase + lhs].minY < curves[curveBase + rhs].minY;
});
float splitCoord = bestSplitCoord(positive, false);
uint32_t posOffset = static_cast<uint32_t>(vIndices.size());
for (uint32_t index : positive)
{
vIndices.push_back(curveBase + index);
}
uint32_t negOffset = static_cast<uint32_t>(vIndices.size());
for (uint32_t index : negative)
{
vIndices.push_back(curveBase + index);
}
vHeaders.push_back(BandHeader { header.count, posOffset, negOffset, splitCoord });
}
return info;
}
template <typename PointTransform, typename NormalTransform>
static void appendGlyphPolygonVertices(const GlyphPreparation &prep,
const LiveFrameGlyphInfo &info,
const simd_float4 &jacobian,
PointTransform pointTransform,
NormalTransform normalTransform,
std::vector<LiveGlyphVertex> &outVertices)
{
if (prep.polygonVertices.size() < 3)
{
return;
}
auto appendVertex = [&](const PolygonVertex &vertex) {
simd_float2 position = pointTransform(vertex);
simd_float2 normal = normalTransform(vertex);
outVertices.push_back(LiveGlyphVertex {
{ position.x, position.y, normal.x, normal.y },
{ vertex.position[0], vertex.position[1] },
jacobian,
{ info.prep->config.vScale, info.prep->config.hScale, info.prep->config.vOffset, info.prep->config.hOffset },
{ info.hHeaderBase, info.vHeaderBase, info.prep->config.hBandCount, info.prep->config.vBandCount }
});
};
const PolygonVertex &base = prep.polygonVertices.front();
for (size_t i = 1; i + 1 < prep.polygonVertices.size(); ++i)
{
appendVertex(base);
appendVertex(prep.polygonVertices[i]);
appendVertex(prep.polygonVertices[i + 1]);
}
}
class LiveParagraphRenderer
{
public:
LiveParagraphRenderer(const std::vector<fs::path> &fontPaths, uint32_t sizePixels, uint32_t paragraphWidth)
: sizePixels_(sizePixels), paragraphWidth_(paragraphWidth)
{
if (FT_Init_FreeType(&ftLibrary_) != 0)
{
throw std::runtime_error("FT_Init_FreeType failed");
}
fonts_ = loadFonts(ftLibrary_, fontPaths, sizePixels_);
spans_ = buildParagraphSpans(fonts_);
tokens_ = tokenizeSpans(spans_);
}
~LiveParagraphRenderer()
{
destroyFonts(fonts_);
if (ftLibrary_ != nullptr)
{
FT_Done_FreeType(ftLibrary_);
ftLibrary_ = nullptr;
}
}
LiveFrame buildFrame()
{
LiveFrame frame;
auto shapeStart = std::chrono::steady_clock::now();
std::vector<ShapedToken> shapedTokens;
shapedTokens.reserve(tokens_.size());
for (const auto &token : tokens_)
{
shapedTokens.push_back(shapeToken(fonts_[token.fontIndex], token));
}
auto shapeEnd = std::chrono::steady_clock::now();
ParagraphLayout layout = layoutParagraph(fonts_, shapedTokens, paragraphWidth_);
frame.shapeMs = std::chrono::duration<double, std::milli>(shapeEnd - shapeStart).count();
frame.glyphCount = layout.glyphs.size();
float pageWidth = layout.canvasWidth;
float pageHeight = layout.canvasHeight;
frame.pageAspect = pageWidth / std::max(pageHeight, 1.0f);
float sceneScale = 2.4f / std::max(pageWidth, pageHeight);
float inverseScale = 1.0f / sceneScale;
float centerX = pageWidth * 0.5f;
float centerY = pageHeight * 0.5f;
auto makeWorldX = [&](float pageX) {
return (pageX - centerX) * sceneScale;
};
auto makeWorldY = [&](float pageY) {
return (centerY - pageY) * sceneScale;
};
float left = makeWorldX(0.0f);
float right = makeWorldX(pageWidth);
float top = makeWorldY(0.0f);
float bottom = makeWorldY(pageHeight);
constexpr float kPageDepth = -0.02f;
frame.pageVertices = {
{ { left, bottom, kPageDepth }, { 0.0f, 1.0f } },
{ { right, bottom, kPageDepth }, { 1.0f, 1.0f } },
{ { left, top, kPageDepth }, { 0.0f, 0.0f } },
{ { left, top, kPageDepth }, { 0.0f, 0.0f } },
{ { right, bottom, kPageDepth }, { 1.0f, 1.0f } },
{ { right, top, kPageDepth }, { 1.0f, 0.0f } },
};
std::map<GlyphKey, LiveFrameGlyphInfo> frameGlyphs;
const simd_float4 jacobian = { inverseScale, 0.0f, 0.0f, -inverseScale };
auto prepStart = std::chrono::steady_clock::now();
for (const auto &placed : layout.glyphs)
{
GlyphKey key { placed.fontIndex, placed.glyphIndex };
auto found = frameGlyphs.find(key);
if (found == frameGlyphs.end())
{
GlyphPreparation &prep = ensureGlyphPrep(placed.fontIndex, placed.glyphIndex);
found = frameGlyphs.emplace(key,
appendFrameGlyphData(prep,
frame.curves,
frame.hHeaders,
frame.hIndices,
frame.vHeaders,
frame.vIndices)).first;
}
const LiveFrameGlyphInfo &info = found->second;
const GlyphPreparation &prep = *info.prep;
if (prep.curves.empty())
{
continue;
}
appendGlyphPolygonVertices(prep,
info,
jacobian,
[&](const PolygonVertex &vertex) {
float pageX = placed.originX + vertex.position[0];
float pageY = placed.originY - vertex.position[1];
return simd_make_float2(makeWorldX(pageX), makeWorldY(pageY));
},
[&](const PolygonVertex &vertex) {
return simd_make_float2(vertex.normal[0] * sceneScale, -vertex.normal[1] * sceneScale);
},
frame.glyphVertices);
}
auto prepEnd = std::chrono::steady_clock::now();
frame.prepMs = std::chrono::duration<double, std::milli>(prepEnd - prepStart).count();
frame.uniqueGlyphCount = frameGlyphs.size();
return frame;
}
private:
GlyphPreparation &ensureGlyphPrep(size_t fontIndex, uint32_t glyphIndex)
{
GlyphKey key { fontIndex, glyphIndex };
auto found = glyphPrepCache_.find(key);
if (found == glyphPrepCache_.end())
{
found = glyphPrepCache_.emplace(key, prepareGlyph(fonts_[fontIndex].face, glyphIndex, sizePixels_)).first;
}
return found->second;
}
FT_Library ftLibrary_ = nullptr;
std::vector<FontResource> fonts_;
std::vector<StyledSpan> spans_;
std::vector<TextToken> tokens_;
std::map<GlyphKey, GlyphPreparation> glyphPrepCache_;
uint32_t sizePixels_ = 0;
uint32_t paragraphWidth_ = 0;
};
class HudTextRenderer
{
public:
HudTextRenderer(const fs::path &fontPath, uint32_t sizePixels) : sizePixels_(sizePixels)
{
if (FT_Init_FreeType(&ftLibrary_) != 0)
{
throw std::runtime_error("FT_Init_FreeType failed for HUD");
}
fonts_ = loadFonts(ftLibrary_, std::vector<fs::path> { fontPath }, sizePixels_);
if (fonts_.empty())
{
throw std::runtime_error("Failed to load Mohave HUD font");
}
}
~HudTextRenderer()
{
destroyFonts(fonts_);
if (ftLibrary_ != nullptr)
{
FT_Done_FreeType(ftLibrary_);
ftLibrary_ = nullptr;
}
}
HudFrame buildFrame(std::string_view text) const
{
HudFrame frame;
if (text.empty())
{
return frame;
}
const FontResource &font = fonts_.front();
const float marginX = 18.0f;
const float marginY = 18.0f;
const float lineAdvance = std::max(font.ascentPx + font.descentPx + font.lineGapPx, static_cast<float>(sizePixels_) + 4.0f);
const simd_float4 jacobian = { 1.0f, 0.0f, 0.0f, -1.0f };
std::map<uint32_t, LiveFrameGlyphInfo> frameGlyphs;
size_t lineStart = 0;
uint32_t lineIndex = 0;
while (lineStart <= text.size())
{
size_t lineEnd = text.find('\n', lineStart);
if (lineEnd == std::string_view::npos)
{
lineEnd = text.size();
}
std::string line(text.substr(lineStart, lineEnd - lineStart));
TextToken token { 0, line, false, false };
ShapedToken shaped = shapeToken(font, token);
float originX = marginX;
float baselineY = marginY + font.ascentPx + lineAdvance * static_cast<float>(lineIndex);
for (const auto &glyph : shaped.glyphs)
{
auto found = frameGlyphs.find(glyph.glyphIndex);
if (found == frameGlyphs.end())
{
const GlyphPreparation &prep = ensureGlyphPrep(glyph.glyphIndex);
found = frameGlyphs.emplace(glyph.glyphIndex,
appendFrameGlyphData(prep,
frame.curves,
frame.hHeaders,
frame.hIndices,
frame.vHeaders,
frame.vIndices)).first;
}
const LiveFrameGlyphInfo &info = found->second;
const GlyphPreparation &prep = *info.prep;
if (prep.curves.empty())
{
continue;
}
float glyphOriginX = originX + glyph.offsetX;
float glyphOriginY = baselineY - glyph.offsetY;
appendGlyphPolygonVertices(prep,
info,
jacobian,
[&](const PolygonVertex &vertex) {
return simd_make_float2(glyphOriginX + vertex.position[0],
glyphOriginY - vertex.position[1]);
},
[&](const PolygonVertex &vertex) {
return simd_make_float2(vertex.normal[0], -vertex.normal[1]);
},
frame.glyphVertices);
}
if (lineEnd == text.size())
{
break;
}
lineStart = lineEnd + 1;
++lineIndex;
}
return frame;
}
private:
const GlyphPreparation &ensureGlyphPrep(uint32_t glyphIndex) const
{
auto found = glyphPrepCache_.find(glyphIndex);
if (found == glyphPrepCache_.end())
{
found = glyphPrepCache_.emplace(glyphIndex, prepareGlyph(fonts_.front().face, glyphIndex, sizePixels_)).first;
}
return found->second;
}
mutable std::map<uint32_t, GlyphPreparation> glyphPrepCache_;
FT_Library ftLibrary_ = nullptr;
std::vector<FontResource> fonts_;
uint32_t sizePixels_ = 0;
};
static std::string formatHudText(const HudMetrics &metrics)
{
std::ostringstream out;
out << std::fixed << std::setprecision(1);
out << "FPS " << metrics.fps << '\n';
out << "CPU Frame " << std::setprecision(2) << metrics.cpuFrameMs << " ms\n";
out << "GPU Frame " << std::setprecision(2) << metrics.gpuFrameMs << " ms\n";
out << "Scene Shape " << std::setprecision(2) << metrics.sceneShapeMs << " ms\n";
out << "Scene Prep " << std::setprecision(2) << metrics.scenePrepMs << " ms\n";
out << "Glyphs " << metrics.glyphCount << " / " << metrics.uniqueGlyphCount;
return out.str();
}
class SceneRendererCore
{
public:
SceneRendererCore(id<MTLDevice> device,
LiveParagraphRenderer &liveRenderer,
MTLPixelFormat colorFormat,
MTLPixelFormat depthFormat)
: device_(device)
{
queue_ = [device_ newCommandQueue];
NSError *error = nil;
NSString *shaderSource = [NSString stringWithUTF8String:kShaderSource];
id<MTLLibrary> library = [device_ newLibraryWithSource:shaderSource options:nil error:&error];
if (library == nil)
{
throw std::runtime_error([[error localizedDescription] UTF8String]);
}
id<MTLFunction> pageVertexFunction = [library newFunctionWithName:@"page_vertex"];
id<MTLFunction> pageBackgroundFunction = [library newFunctionWithName:@"page_background_fragment"];
id<MTLFunction> glyphVertexFunction = [library newFunctionWithName:@"slug_live_vertex"];
id<MTLFunction> glyphFragmentFunction = [library newFunctionWithName:@"slug_live_fragment"];
MTLVertexDescriptor *pageVertexDescriptor = [[MTLVertexDescriptor alloc] init];
pageVertexDescriptor.attributes[0].format = MTLVertexFormatFloat3;
pageVertexDescriptor.attributes[0].offset = offsetof(PageVertex, position);
pageVertexDescriptor.attributes[0].bufferIndex = 0;
pageVertexDescriptor.attributes[1].format = MTLVertexFormatFloat2;
pageVertexDescriptor.attributes[1].offset = offsetof(PageVertex, texCoord);
pageVertexDescriptor.attributes[1].bufferIndex = 0;
pageVertexDescriptor.layouts[0].stride = sizeof(PageVertex);
pageVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
MTLRenderPipelineDescriptor *backgroundDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
backgroundDescriptor.vertexFunction = pageVertexFunction;
backgroundDescriptor.fragmentFunction = pageBackgroundFunction;
backgroundDescriptor.vertexDescriptor = pageVertexDescriptor;
backgroundDescriptor.colorAttachments[0].pixelFormat = colorFormat;
backgroundDescriptor.depthAttachmentPixelFormat = depthFormat;
backgroundPipeline_ = [device_ newRenderPipelineStateWithDescriptor:backgroundDescriptor error:&error];
if (backgroundPipeline_ == nil)
{
throw std::runtime_error([[error localizedDescription] UTF8String]);
}
MTLVertexDescriptor *glyphVertexDescriptor = [[MTLVertexDescriptor alloc] init];
glyphVertexDescriptor.attributes[0].format = MTLVertexFormatFloat4;
glyphVertexDescriptor.attributes[0].offset = offsetof(LiveGlyphVertex, posNorm);
glyphVertexDescriptor.attributes[0].bufferIndex = 0;
glyphVertexDescriptor.attributes[1].format = MTLVertexFormatFloat2;
glyphVertexDescriptor.attributes[1].offset = offsetof(LiveGlyphVertex, sampleCoord);
glyphVertexDescriptor.attributes[1].bufferIndex = 0;
glyphVertexDescriptor.attributes[2].format = MTLVertexFormatFloat4;
glyphVertexDescriptor.attributes[2].offset = offsetof(LiveGlyphVertex, jacobian);
glyphVertexDescriptor.attributes[2].bufferIndex = 0;
glyphVertexDescriptor.attributes[3].format = MTLVertexFormatFloat4;
glyphVertexDescriptor.attributes[3].offset = offsetof(LiveGlyphVertex, banding);
glyphVertexDescriptor.attributes[3].bufferIndex = 0;
glyphVertexDescriptor.attributes[4].format = MTLVertexFormatUInt4;
glyphVertexDescriptor.attributes[4].offset = offsetof(LiveGlyphVertex, glyph);
glyphVertexDescriptor.attributes[4].bufferIndex = 0;
glyphVertexDescriptor.layouts[0].stride = sizeof(LiveGlyphVertex);
glyphVertexDescriptor.layouts[0].stepFunction = MTLVertexStepFunctionPerVertex;
MTLRenderPipelineDescriptor *glyphDescriptor = [[MTLRenderPipelineDescriptor alloc] init];
glyphDescriptor.vertexFunction = glyphVertexFunction;
glyphDescriptor.fragmentFunction = glyphFragmentFunction;
glyphDescriptor.vertexDescriptor = glyphVertexDescriptor;
glyphDescriptor.colorAttachments[0].pixelFormat = colorFormat;
glyphDescriptor.depthAttachmentPixelFormat = depthFormat;
glyphDescriptor.colorAttachments[0].blendingEnabled = YES;
glyphDescriptor.colorAttachments[0].rgbBlendOperation = MTLBlendOperationAdd;
glyphDescriptor.colorAttachments[0].alphaBlendOperation = MTLBlendOperationAdd;
glyphDescriptor.colorAttachments[0].sourceRGBBlendFactor = MTLBlendFactorSourceAlpha;
glyphDescriptor.colorAttachments[0].sourceAlphaBlendFactor = MTLBlendFactorSourceAlpha;
glyphDescriptor.colorAttachments[0].destinationRGBBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
glyphDescriptor.colorAttachments[0].destinationAlphaBlendFactor = MTLBlendFactorOneMinusSourceAlpha;
glyphPipeline_ = [device_ newRenderPipelineStateWithDescriptor:glyphDescriptor error:&error];
if (glyphPipeline_ == nil)
{
throw std::runtime_error([[error localizedDescription] UTF8String]);
}
MTLDepthStencilDescriptor *depthDescriptor = [[MTLDepthStencilDescriptor alloc] init];
depthDescriptor.depthCompareFunction = MTLCompareFunctionLessEqual;
depthDescriptor.depthWriteEnabled = YES;
depthState_ = [device_ newDepthStencilStateWithDescriptor:depthDescriptor];
MTLDepthStencilDescriptor *hudDepthDescriptor = [[MTLDepthStencilDescriptor alloc] init];
hudDepthDescriptor.depthCompareFunction = MTLCompareFunctionAlways;
hudDepthDescriptor.depthWriteEnabled = NO;
hudDepthState_ = [device_ newDepthStencilStateWithDescriptor:hudDepthDescriptor];
LiveFrame frame = liveRenderer.buildFrame();
sceneShapeMs_ = frame.shapeMs;
scenePrepMs_ = frame.prepMs;
sceneGlyphCount_ = frame.glyphCount;
sceneUniqueGlyphCount_ = frame.uniqueGlyphCount;
pageBuffer_ = createPrivateBuffer(device_, queue_, frame.pageVertices);
glyphBuffer_ = createPrivateBuffer(device_, queue_, frame.glyphVertices);
curves_ = createPrivateBuffer(device_, queue_, frame.curves);
hHeaders_ = createPrivateBuffer(device_, queue_, frame.hHeaders);
hIndices_ = createPrivateBuffer(device_, queue_, frame.hIndices);
vHeaders_ = createPrivateBuffer(device_, queue_, frame.vHeaders);
vIndices_ = createPrivateBuffer(device_, queue_, frame.vIndices);
pageVertexCount_ = static_cast<NSUInteger>(frame.pageVertices.size());
glyphVertexCount_ = static_cast<NSUInteger>(frame.glyphVertices.size());
}
id<MTLCommandQueue> commandQueue() const
{
return queue_;
}
double sceneShapeMs() const
{
return sceneShapeMs_;
}
double scenePrepMs() const
{
return scenePrepMs_;
}
size_t sceneGlyphCount() const
{
return sceneGlyphCount_;
}
size_t sceneUniqueGlyphCount() const
{
return sceneUniqueGlyphCount_;
}
SceneUniforms sceneUniforms(CGSize size, const CameraState &camera) const
{
SceneUniforms uniforms {};
float aspect = std::max(0.1f, static_cast<float>(size.width / std::max(size.height, 1.0)));
simd_float3 eye = {
camera.distance * std::cos(camera.pitch) * std::sin(camera.yaw),
camera.distance * std::sin(camera.pitch),
camera.distance * std::cos(camera.pitch) * std::cos(camera.yaw)
};
simd_float4x4 projection = MatrixPerspective(0.78f, aspect, 0.1f, 50.0f);
simd_float4x4 view = MatrixLookAt(eye, simd_make_float3(0.0f, 0.0f, 0.0f), simd_make_float3(0.0f, 1.0f, 0.0f));
simd_float4x4 mvp = simd_mul(projection, view);
uniforms.row0 = { mvp.columns[0].x, mvp.columns[1].x, mvp.columns[2].x, mvp.columns[3].x };
uniforms.row1 = { mvp.columns[0].y, mvp.columns[1].y, mvp.columns[2].y, mvp.columns[3].y };
uniforms.row2 = { mvp.columns[0].z, mvp.columns[1].z, mvp.columns[2].z, mvp.columns[3].z };
uniforms.row3 = { mvp.columns[0].w, mvp.columns[1].w, mvp.columns[2].w, mvp.columns[3].w };
uniforms.viewport = { static_cast<float>(size.width), static_cast<float>(size.height) };
uniforms.inkColor = simd_make_float4(0.98f, 0.93f, 0.84f, 1.0f);
uniforms.paperColor = simd_make_float4(0.09f, 0.10f, 0.11f, 1.0f);
return uniforms;
}
SceneUniforms hudUniforms(CGSize size) const
{
SceneUniforms uniforms {};
float width = std::max(1.0f, static_cast<float>(size.width));
float height = std::max(1.0f, static_cast<float>(size.height));
uniforms.row0 = { 2.0f / width, 0.0f, 0.0f, -1.0f };
uniforms.row1 = { 0.0f, -2.0f / height, 0.0f, 1.0f };
uniforms.row2 = { 0.0f, 0.0f, 1.0f, 0.0f };
uniforms.row3 = { 0.0f, 0.0f, 0.0f, 1.0f };
uniforms.viewport = { width, height };
uniforms.inkColor = simd_make_float4(0.96f, 0.97f, 0.99f, 0.96f);
uniforms.paperColor = simd_make_float4(0.0f, 0.0f, 0.0f, 0.0f);
return uniforms;
}
void encodeFrame(id<MTLRenderCommandEncoder> encoder,
CGSize size,
const CameraState &camera,
HudTextRenderer *hudRenderer,
const HudMetrics *hudMetrics)
{
SceneUniforms uniforms = sceneUniforms(size, camera);
[encoder setCullMode:MTLCullModeNone];
[encoder setDepthStencilState:depthState_];
[encoder setRenderPipelineState:backgroundPipeline_];
[encoder setVertexBuffer:pageBuffer_ offset:0 atIndex:0];
[encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1];
[encoder setFragmentBytes:&uniforms length:sizeof(uniforms) atIndex:1];
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:pageVertexCount_];
if (glyphVertexCount_ == 0)
{
return;
}
[encoder setRenderPipelineState:glyphPipeline_];
[encoder setVertexBuffer:glyphBuffer_ offset:0 atIndex:0];
[encoder setVertexBytes:&uniforms length:sizeof(uniforms) atIndex:1];
[encoder setFragmentBytes:&uniforms length:sizeof(uniforms) atIndex:1];
[encoder setFragmentBuffer:curves_ offset:0 atIndex:2];
[encoder setFragmentBuffer:hHeaders_ offset:0 atIndex:3];
[encoder setFragmentBuffer:hIndices_ offset:0 atIndex:4];
[encoder setFragmentBuffer:vHeaders_ offset:0 atIndex:5];
[encoder setFragmentBuffer:vIndices_ offset:0 atIndex:6];
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:glyphVertexCount_];
if (hudRenderer != nullptr && hudMetrics != nullptr)
{
HudFrame hudFrame = hudRenderer->buildFrame(formatHudText(*hudMetrics));
id<MTLBuffer> hudGlyphBuffer = createSharedBuffer(device_, hudFrame.glyphVertices);
id<MTLBuffer> hudCurves = createSharedBuffer(device_, hudFrame.curves);
id<MTLBuffer> hudHHeaders = createSharedBuffer(device_, hudFrame.hHeaders);
id<MTLBuffer> hudHIndices = createSharedBuffer(device_, hudFrame.hIndices);
id<MTLBuffer> hudVHeaders = createSharedBuffer(device_, hudFrame.vHeaders);
id<MTLBuffer> hudVIndices = createSharedBuffer(device_, hudFrame.vIndices);
SceneUniforms hudValues = hudUniforms(size);
[encoder setDepthStencilState:hudDepthState_];
[encoder setRenderPipelineState:glyphPipeline_];
[encoder setVertexBuffer:hudGlyphBuffer offset:0 atIndex:0];
[encoder setVertexBytes:&hudValues length:sizeof(hudValues) atIndex:1];
[encoder setFragmentBytes:&hudValues length:sizeof(hudValues) atIndex:1];
[encoder setFragmentBuffer:hudCurves offset:0 atIndex:2];
[encoder setFragmentBuffer:hudHHeaders offset:0 atIndex:3];
[encoder setFragmentBuffer:hudHIndices offset:0 atIndex:4];
[encoder setFragmentBuffer:hudVHeaders offset:0 atIndex:5];
[encoder setFragmentBuffer:hudVIndices offset:0 atIndex:6];
[encoder drawPrimitives:MTLPrimitiveTypeTriangle vertexStart:0 vertexCount:(NSUInteger)hudFrame.glyphVertices.size()];
}
}
private:
id<MTLDevice> device_ = nil;
id<MTLCommandQueue> queue_ = nil;
id<MTLRenderPipelineState> backgroundPipeline_ = nil;
id<MTLRenderPipelineState> glyphPipeline_ = nil;
id<MTLDepthStencilState> depthState_ = nil;
id<MTLDepthStencilState> hudDepthState_ = nil;
id<MTLBuffer> pageBuffer_ = nil;
id<MTLBuffer> glyphBuffer_ = nil;
id<MTLBuffer> curves_ = nil;
id<MTLBuffer> hHeaders_ = nil;
id<MTLBuffer> hIndices_ = nil;
id<MTLBuffer> vHeaders_ = nil;
id<MTLBuffer> vIndices_ = nil;
NSUInteger pageVertexCount_ = 0;
NSUInteger glyphVertexCount_ = 0;
double sceneShapeMs_ = 0.0;
double scenePrepMs_ = 0.0;
size_t sceneGlyphCount_ = 0;
size_t sceneUniqueGlyphCount_ = 0;
};
} // namespace slug
@interface ParagraphSceneRenderer : NSObject <MTKViewDelegate>
- (instancetype)initWithView:(MTKView *)view runtime:(slug::AppRuntime *)runtime;
- (void)rotateByDeltaX:(float)deltaX deltaY:(float)deltaY;
- (void)zoomBy:(float)delta;
- (void)resetCamera;
@end
@implementation ParagraphSceneRenderer
{
__weak MTKView *_view;
slug::AppRuntime *_runtime;
std::unique_ptr<slug::SceneRendererCore> _core;
slug::CameraState _camera;
CFTimeInterval _lastFrameTime;
double _smoothedFps;
slug::HudMetrics _lastMetrics;
}
- (instancetype)initWithView:(MTKView *)view runtime:(slug::AppRuntime *)runtime
{
self = [super init];
if (!self)
{
return nil;
}
_view = view;
_runtime = runtime;
_camera = slug::CameraState {};
_core = std::make_unique<slug::SceneRendererCore>(view.device,
*runtime->liveRenderer,
view.colorPixelFormat,
view.depthStencilPixelFormat);
return self;
}
- (void)resetCamera
{
_camera = slug::CameraState {};
}
- (void)rotateByDeltaX:(float)deltaX deltaY:(float)deltaY
{
_camera.yaw += deltaX * 0.01f;
_camera.pitch += deltaY * 0.01f;
_camera.pitch = std::clamp(_camera.pitch, -1.2f, 1.2f);
}
- (void)zoomBy:(float)delta
{
float factor = std::exp(delta * 0.01f);
_camera.distance = std::clamp(_camera.distance * factor, 1.2f, 10.0f);
}
- (void)mtkView:(MTKView *)view drawableSizeWillChange:(CGSize)size
{
(void)view;
(void)size;
}
- (void)drawInMTKView:(MTKView *)view
{
if (view.currentDrawable == nil || view.currentRenderPassDescriptor == nil)
{
return;
}
CFTimeInterval frameStart = CACurrentMediaTime();
if (_lastFrameTime > 0.0)
{
double deltaSeconds = std::max(frameStart - _lastFrameTime, 1.0 / 10000.0);
double instantFps = 1.0 / deltaSeconds;
_smoothedFps = (_smoothedFps <= 0.0) ? instantFps : (_smoothedFps * 0.9 + instantFps * 0.1);
}
_lastFrameTime = frameStart;
slug::HudMetrics displayMetrics = _lastMetrics;
displayMetrics.fps = _smoothedFps;
displayMetrics.sceneShapeMs = _core->sceneShapeMs();
displayMetrics.scenePrepMs = _core->scenePrepMs();
displayMetrics.glyphCount = _core->sceneGlyphCount();
displayMetrics.uniqueGlyphCount = _core->sceneUniqueGlyphCount();
id<MTLCommandBuffer> commandBuffer = [_core->commandQueue() commandBuffer];
id<MTLRenderCommandEncoder> encoder = [commandBuffer renderCommandEncoderWithDescriptor:view.currentRenderPassDescriptor];
_core->encodeFrame(encoder, view.drawableSize, _camera, _runtime->hudRenderer.get(), &displayMetrics);
[encoder endEncoding];
CFTimeInterval frameEnd = CACurrentMediaTime();
_lastMetrics = displayMetrics;
_lastMetrics.cpuFrameMs = (frameEnd - frameStart) * 1000.0;
__weak ParagraphSceneRenderer *weakSelf = self;
[commandBuffer addCompletedHandler:^(id<MTLCommandBuffer> completedBuffer) {
double gpuMs = 0.0;
if (completedBuffer.GPUStartTime > 0.0 &&
completedBuffer.GPUEndTime > completedBuffer.GPUStartTime)
{
gpuMs = (completedBuffer.GPUEndTime - completedBuffer.GPUStartTime) * 1000.0;
}
dispatch_async(dispatch_get_main_queue(), ^{
ParagraphSceneRenderer *strongSelf = weakSelf;
if (strongSelf != nil)
{
strongSelf->_lastMetrics.gpuFrameMs = gpuMs;
}
});
}];
[commandBuffer presentDrawable:view.currentDrawable];
[commandBuffer commit];
}
@end
@interface ParagraphView : MTKView
@property(nonatomic, strong) ParagraphSceneRenderer *sceneRenderer;
@end
@implementation ParagraphView
{
NSPoint _lastPoint;
}
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)mouseDown:(NSEvent *)event
{
_lastPoint = [self convertPoint:event.locationInWindow fromView:nil];
}
- (void)mouseDragged:(NSEvent *)event
{
NSPoint point = [self convertPoint:event.locationInWindow fromView:nil];
CGFloat deltaX = point.x - _lastPoint.x;
CGFloat deltaY = point.y - _lastPoint.y;
[self.sceneRenderer rotateByDeltaX:(float)deltaX deltaY:(float)deltaY];
_lastPoint = point;
}
- (void)scrollWheel:(NSEvent *)event
{
[self.sceneRenderer zoomBy:(float)(-event.scrollingDeltaY)];
}
- (void)magnifyWithEvent:(NSEvent *)event
{
[self.sceneRenderer zoomBy:(float)(-event.magnification * 120.0)];
}
- (void)keyDown:(NSEvent *)event
{
NSString *characters = event.charactersIgnoringModifiers.lowercaseString;
if ([characters isEqualToString:@"r"])
{
[self.sceneRenderer resetCamera];
}
else
{
[super keyDown:event];
}
}
@end
static void CreateMenuBar(void)
{
NSMenu *menuBar = [[NSMenu alloc] init];
NSMenuItem *appMenuItem = [[NSMenuItem alloc] init];
[menuBar addItem:appMenuItem];
[NSApp setMainMenu:menuBar];
NSMenu *appMenu = [[NSMenu alloc] initWithTitle:@"Slug"];
NSString *quitTitle = [@"Quit " stringByAppendingString:NSProcessInfo.processInfo.processName];
NSMenuItem *quitItem = [[NSMenuItem alloc] initWithTitle:quitTitle
action:@selector(terminate:)
keyEquivalent:@"q"];
[appMenu addItem:quitItem];
[appMenuItem setSubmenu:appMenu];
}
@interface AppDelegate : NSObject <NSApplicationDelegate, NSWindowDelegate>
@property(nonatomic) slug::AppRuntime *runtime;
@property(nonatomic, strong) NSWindow *window;
@property(nonatomic, strong) ParagraphView *view;
@property(nonatomic, strong) ParagraphSceneRenderer *renderer;
@end
@implementation AppDelegate
- (void)applicationDidFinishLaunching:(NSNotification *)notification
{
(void)notification;
CreateMenuBar();
NSRect frame = NSMakeRect(0.0, 0.0, 1280.0, 860.0);
self.window = [[NSWindow alloc] initWithContentRect:frame
styleMask:(NSWindowStyleMaskTitled |
NSWindowStyleMaskClosable |
NSWindowStyleMaskMiniaturizable |
NSWindowStyleMaskResizable)
backing:NSBackingStoreBuffered
defer:NO];
self.window.delegate = self;
self.window.title = @"Slug Text Renderer drag to orbit scroll to zoom R reset";
self.view = [[ParagraphView alloc] initWithFrame:self.window.contentView.bounds device:self.runtime->device];
self.view.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
self.view.colorPixelFormat = MTLPixelFormatBGRA8Unorm_sRGB;
self.view.depthStencilPixelFormat = MTLPixelFormatDepth32Float;
self.view.framebufferOnly = YES;
self.view.clearColor = MTLClearColorMake(0.035, 0.040, 0.050, 1.0);
self.view.preferredFramesPerSecond = 60;
self.view.paused = NO;
self.view.enableSetNeedsDisplay = NO;
self.renderer = [[ParagraphSceneRenderer alloc] initWithView:self.view runtime:self.runtime];
self.view.delegate = self.renderer;
self.view.sceneRenderer = self.renderer;
[self.window.contentView addSubview:self.view];
[self.window center];
[self.window makeKeyAndOrderFront:nil];
[self.window makeFirstResponder:self.view];
[NSApp activateIgnoringOtherApps:YES];
}
- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)sender
{
(void)sender;
return YES;
}
- (void)windowWillClose:(NSNotification *)notification
{
(void)notification;
[NSApp terminate:nil];
}
@end
namespace slug
{
static id gAppDelegate = nil;
static int runApp(AppRuntime &runtime)
{
NSApplication *application = [NSApplication sharedApplication];
[application setActivationPolicy:NSApplicationActivationPolicyRegular];
AppDelegate *delegate = [[AppDelegate alloc] init];
delegate.runtime = &runtime;
gAppDelegate = delegate;
application.delegate = delegate;
[application run];
gAppDelegate = nil;
return 0;
}
} // namespace slug
int main()
{
@autoreleasepool
{
try
{
slug::fs::path root = slug::locateProjectRoot();
std::vector<slug::fs::path> fontPaths = slug::discoverFonts(root);
if (fontPaths.empty())
{
throw std::runtime_error("No .ttf fonts found in project root");
}
slug::AppRuntime runtime;
runtime.device = MTLCreateSystemDefaultDevice();
if (runtime.device == nil)
{
throw std::runtime_error("Metal device unavailable");
}
runtime.liveRenderer = std::make_unique<slug::LiveParagraphRenderer>(fontPaths,
slug::kFontSizePixels,
slug::kParagraphWidth);
slug::fs::path hudFontPath = root / "Mohave-Regular.ttf";
if (!slug::fs::exists(hudFontPath))
{
throw std::runtime_error("Mohave-Regular.ttf is required for the HUD overlay");
}
runtime.hudRenderer = std::make_unique<slug::HudTextRenderer>(hudFontPath, 20);
return slug::runApp(runtime);
}
catch (const std::exception &exception)
{
std::cerr << exception.what() << '\n';
return 1;
}
catch (NSException *exception)
{
std::cerr << exception.reason.UTF8String << '\n';
return 1;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment