glyph cache progress

This commit is contained in:
jacob 2025-12-12 19:50:44 -06:00
parent 5fd73b7911
commit c8a9970438
14 changed files with 1138 additions and 304 deletions

View File

@ -93,12 +93,12 @@ void *PushBytesNoZero(Arena *arena, u64 size, u64 align)
Assert(align > 0);
u8 *base = ArenaFirst(arena, u8);
u64 start_pos = AlignU64(arena->pos, align);
u64 end_pos = start_pos + size;
void *result = base + start_pos;
/* Commit new block(s) */
if (end_pos > arena->committed)
if (size > 0 && end_pos > arena->committed)
{
u64 blocks_needed = (end_pos - arena->committed + ArenaBlockSize - 1) / ArenaBlockSize;
u64 commit_bytes = blocks_needed * ArenaBlockSize;
@ -119,8 +119,7 @@ void *PushBytesNoZero(Arena *arena, u64 size, u64 align)
AsanPoison(commit_address, commit_bytes);
}
void *result = base + start_pos;
AsanUnpoison(result, end_pos - start_pos);
AsanUnpoison(result, size);
arena->pos = end_pos;
return result;

View File

@ -10,10 +10,10 @@
# include "base_arena.h"
# include "base_futex.h"
# include "base_sync.h"
# include "base_wave.h"
# include "base_time.h"
# include "base_uid.h"
# include "base_math.h"
# include "base_wave.h"
# include "base_string.h"
# include "base_cmdline.h"
# include "base_log.h"
@ -36,13 +36,13 @@
# include "base_memory.c"
# include "base_arena.c"
# include "base_sync.c"
# include "base_wave.c"
# include "base_uid.c"
# include "base_string.c"
# include "base_cmdline.c"
# include "base_uni.c"
# include "base_buddy.c"
# include "base_math.c"
# include "base_wave.c"
# include "base_rand.c"
# include "base_bitbuff.c"
# include "base_resource.c"

View File

@ -1125,6 +1125,16 @@ Vec2I32 SubVec2I32(Vec2I32 a, Vec2I32 b)
return VEC2I32(a.x - b.x, a.y - b.y);
}
Vec2I32 MulVec2I32Vec2I32(Vec2I32 a, Vec2I32 b)
{
return VEC2I32(a.x * b.x, a.y * b.y);
}
Vec2I32 DivVec2I32Vec2I32(Vec2I32 a, Vec2I32 b)
{
return VEC2I32(a.x / b.x, a.y / b.y);
}
////////////////////////////////////////////////////////////
//~ Vec4
@ -1151,6 +1161,8 @@ u32 U32FromVec4(Vec4 v)
////////////////////////////////////////////////////////////
//~ Range
//- Rng2
Vec2 DimsFromRng2(Rng2 r)
{
Vec2 result = ZI;
@ -1185,6 +1197,43 @@ Rng2 DivRng2Vec2(Rng2 r, Vec2 v)
return result;
}
//- Rng2I32
Vec2I32 DimsFromRng2I32(Rng2I32 r)
{
Vec2I32 result = ZI;
result.x = r.p1.x - r.p0.x;
result.y = r.p1.y - r.p0.y;
return result;
}
Rng2I32 UnionRng2I32(Rng2I32 a, Rng2I32 b)
{
Rng2I32 result = ZI;
result.p0.x = MinF32(a.p0.x, b.p0.x);
result.p0.y = MinF32(a.p0.y, b.p0.y);
result.p1.x = MaxF32(a.p1.x, b.p1.x);
result.p1.y = MaxF32(a.p1.y, b.p1.y);
return result;
}
Rng2I32 AddRng2I32Vec2I32(Rng2I32 r, Vec2I32 v)
{
Rng2I32 result = ZI;
result.p0 = AddVec2I32(result.p0, v);
result.p1 = AddVec2I32(result.p1, v);
return result;
}
Rng2I32 DivRng2I32Vec2I32(Rng2I32 r, Vec2I32 v)
{
Rng2I32 result = ZI;
result.p0 = DivVec2I32Vec2I32(result.p0, v);
result.p1 = DivVec2I32Vec2I32(result.p1, v);
return result;
}
////////////////////////////////////////////////////////////
//~ Xform

View File

@ -388,12 +388,18 @@ u32 U32FromVec4(Vec4 v);
////////////////////////////////////////////////////////////
//~ Range
//- Rng2
Vec2 DimsFromRng2(Rng2 r);
Rng2 UnionRng2(Rng2 a, Rng2 b);
Rng2 AddRng2Vec2(Rng2 r, Vec2 v);
Rng2 DivRng2Vec2(Rng2 a, Vec2 v);
//- Rng2I32
Vec2I32 DimsFromRng2I32(Rng2I32 r);
Rng2I32 UnionRng2I32(Rng2I32 a, Rng2I32 b);
Rng2I32 AddRng2I32Vec2I32(Rng2I32 r, Vec2I32 v);
Rng2I32 DivRng2I32Vec2I32(Rng2I32 a, Vec2I32 v);
////////////////////////////////////////////////////////////
//~ Xform

View File

@ -108,10 +108,34 @@ void SetWaveLaneDefaultSpin(WaveLaneCtx *lane, u64 n)
}
////////////////////////////////////////////////////////////
//~ Wave task helpers
//~ Task helpers
i32 WaveLaneIdxFromTaskIdx(WaveLaneCtx *lane, u64 task_idx)
{
WaveCtx *wave = lane->wave;
return task_idx % wave->lanes_count;
}
RngU64 WaveIdxRangeFromCount(WaveLaneCtx *lane, u64 tasks_count)
{
u64 lanes_count = lane->wave->lanes_count;
u64 lane_idx = lane->idx;
u64 tasks_per_lane = tasks_count / lanes_count;
u64 tasks_overflow = tasks_count % lanes_count;
u64 start = lane_idx * tasks_per_lane;
u64 end = start + tasks_per_lane;
if (lane_idx < tasks_overflow)
{
start += lane_idx;
end += lane_idx + 1;
}
else
{
start += tasks_overflow;
end += tasks_overflow;
}
return RNGU64(start, end);
}

View File

@ -46,9 +46,10 @@ void WaveSyncBroadcastEx_(WaveLaneCtx *lane, u32 broadcast_lane_idx, void *broad
void SetWaveLaneDefaultSpin(WaveLaneCtx *lane, u64 n);
////////////////////////////////////////////////////////////
//~ Wave task helpers
//~ Task helpers
i32 WaveLaneIdxFromTaskIdx(WaveLaneCtx *lane, u64 task_idx);
RngU64 WaveIdxRangeFromCount(WaveLaneCtx *lane, u64 tasks_count);
////////////////////////////////////////////////////////////
//~ @hookdecl Dispatch

View File

@ -69,8 +69,8 @@
#define FLOOD_DEBUG 0
#define GPU_DEBUG 0
#define GPU_DEBUG_VALIDATION 0
#define GPU_DEBUG 1
#define GPU_DEBUG_VALIDATION 1
#define GPU_SHADER_PRINT 1
#define GPU_SHADER_PRINT_BUFFER_SIZE Kibi(64);

View File

@ -14,13 +14,14 @@ void GC_Bootstrap(void)
GC_FontKey GC_FontKeyFromResource(ResourceKey resource)
{
GC_FontKey result = ZI;
result.v = resource.v;
result.r = resource;
return result;
}
u64 GC_HashFromGlyphDesc(GC_GlyphDesc desc)
{
return RandU64FromSeeds(desc.font.v, ((u64)desc.codepoint << 32) | *(u32 *)&desc.font_size);
/* TODO: Lower font-size precision to prevent unique hashes for slightly-different font sizes */
return RandU64FromSeeds(desc.font.r.v, ((u64)desc.codepoint << 32) | *(u32 *)&desc.font_size);
}
////////////////////////////////////////////////////////////
@ -31,21 +32,33 @@ GC_Run GC_RunFromString(Arena *arena, String str, GC_FontKey font, f32 font_size
{
GC_Run result = ZI;
TempArena scratch = BeginScratch(arena);
Arena *perm = PermArena();
String32 codepoints = String32FromString(scratch.arena, str);
u64 codepoints_count = 0;
u32 *codepoints = 0;
{
String32 str32 = String32FromString(scratch.arena, str);
codepoints_count = str32.len;
codepoints = str32.text;
}
String32 uncached_codepoints = ZI;
uncached_codepoints.text = PushStructsNoZero(scratch.arena, u32, codepoints.len);
//////////////////////////////
//- Grab glyphs from cache
u64 ready_glyphs_count = 0;
GC_Glyph **ready_glyphs = PushStructsNoZero(scratch.arena, GC_Glyph *, str.len);
GC_Glyph **ready_glyphs = PushStructsNoZero(scratch.arena, GC_Glyph *, codepoints_count);
u32 *uncached_codepoints = PushStructsNoZero(scratch.arena, u32, codepoints_count);
u64 uncached_codepoints_count = 0;
u64 pending_glyphs_count = 0;
{
if (codepoints.len > 0)
if (codepoints_count > 0)
{
Lock lock = LockS(&GC.glyphs_mutex);
for (u64 codepoint_idx = 0; codepoint_idx < codepoints.len; ++codepoint_idx)
for (u64 codepoint_idx = 0; codepoint_idx < codepoints_count; ++codepoint_idx)
{
u32 codepoint = codepoints.text[codepoint_idx];
u32 codepoint = codepoints[codepoint_idx];
GC_GlyphDesc desc = ZI;
desc.font = font;
@ -54,20 +67,20 @@ GC_Run GC_RunFromString(Arena *arena, String str, GC_FontKey font, f32 font_size
u64 hash = GC_HashFromGlyphDesc(desc);
GC_GlyphBin *bin = &GC.glyph_bins[hash % countof(GC.glyph_bins)];
GC_Glyph *glyph = bin->first;
for (; glyph; glyph = glyph->next)
{
if (glyph->hash == hash)
{
break;
}
if (glyph->hash == hash) break;
}
if (glyph == 0)
{
uncached_codepoints.text[uncached_codepoints.len] = codepoint;
uncached_codepoints.len += 1;
uncached_codepoints[uncached_codepoints_count] = codepoint;
uncached_codepoints_count += 1;
}
else if (Atomic32Fetch(&glyph->ready) == 0)
{
pending_glyphs_count += 1;
}
else
{
@ -79,10 +92,77 @@ GC_Run GC_RunFromString(Arena *arena, String str, GC_FontKey font, f32 font_size
}
}
if (uncached_codepoints.len > 0)
//////////////////////////////
//- Create cache entries
u64 submit_cmds_count = 0;
GC_Cmd *submit_cmds = PushStructsNoZero(scratch.arena, GC_Cmd, uncached_codepoints_count);
if (uncached_codepoints_count > 0)
{
Lock lock = LockE(&GC.glyphs_mutex);
{
for (u64 uncached_codepoint_idx = 0; uncached_codepoint_idx < uncached_codepoints_count; ++uncached_codepoint_idx)
{
GC_GlyphDesc desc = ZI;
desc.font = font;
desc.font_size = font_size;
desc.codepoint = uncached_codepoints[uncached_codepoint_idx];
u64 hash = GC_HashFromGlyphDesc(desc);
GC_GlyphBin *bin = &GC.glyph_bins[hash % countof(GC.glyph_bins)];
GC_Glyph *glyph = bin->first;
for (; glyph; glyph = glyph->next)
{
if (glyph->hash == hash) break;
}
if (glyph == 0)
{
glyph = PushStruct(perm, GC_Glyph);
glyph->desc = desc;
glyph->hash = hash;
SllStackPush(bin->first, glyph);
/* Create cmd */
{
GC_Cmd *cmd = &submit_cmds[submit_cmds_count];
cmd->glyph = glyph;
++submit_cmds_count;
}
}
}
}
Unlock(&lock);
}
//////////////////////////////
//- Submit cmds
if (submit_cmds_count > 0)
{
Lock lock = LockE(&GC.submit.mutex);
for (u64 cmd_idx = 0; cmd_idx < submit_cmds_count; ++cmd_idx)
{
GC_Cmd *src = &submit_cmds[cmd_idx];
GC_CmdNode *n = GC.submit.first_free;
if (n)
{
SllStackPop(GC.submit.first_free);
ZeroStruct(n);
}
else
{
n = PushStruct(perm, GC_CmdNode);
}
n->cmd = *src;
GC.submit.count += 1;
SllQueuePush(GC.submit.first, GC.submit.last, n);
}
Unlock(&lock);
}
//////////////////////////////
//- Create run from glyphs
f32 baseline_pos = 0;
result.rects = PushStructs(arena, GC_RunRect, ready_glyphs_count);
result.rects_count = ready_glyphs_count;
@ -98,8 +178,7 @@ GC_Run GC_RunFromString(Arena *arena, String str, GC_FontKey font, f32 font_size
rect->baseline_pos = baseline_pos;
rect->advance = glyph->advance;
rect->bounds.p0 = glyph->baseline_offset;
rect->bounds.p1 = AddVec2(rect->bounds.p0, DimsFromRng2(glyph->tex_rect));
rect->bounds = glyph->bounds;
if (glyph_idx == 0)
{
@ -123,7 +202,7 @@ GC_Run GC_RunFromString(Arena *arena, String str, GC_FontKey font, f32 font_size
result.font_cap = glyph->font_cap;
}
result.ready = uncached_codepoints.len == 0;
result.ready = uncached_codepoints_count == 0 && pending_glyphs_count;
EndScratch(scratch);
return result;
@ -137,21 +216,154 @@ void GC_TickAsync(WaveLaneCtx *lane, AsyncTickCtx *tick)
GC_AsyncCtx *async = &GC.async_ctx;
//////////////////////////////
//- Begin tick
//- Collect cmds
/* TODO: Limit cmds processed per-tick */
if (lane->idx == 0)
{
Lock lock = LockE(&GC.submitted_cmds_mutex);
Lock lock = LockE(&GC.submit.mutex);
{
async->cmds.count = GC.submitted_cmds_count;
async->cmds.v = PushStructsNoZero(tick->arena, GC_Cmd, GC.submitted_cmds_count);
/* Pop cmds from submission queue */
async->cmds.count = GC.submit.count;
async->cmds.v = PushStructsNoZero(tick->arena, GC_Cmd, GC.submit.count);
u64 cmd_idx = 0;
for (GC_CmdNode *n = GC.submit.first; n; n = n->next)
{
async->cmds.v[cmd_idx] = n->cmd;
++cmd_idx;
}
/* Reset submission queue */
GC.submit.first_free = GC.submit.first;
GC.submit.count = 0;
GC.submit.first = 0;
GC.submit.last = 0;
}
Unlock(&lock);
}
WaveSync(lane);
//////////////////////////////
//- Allocate atlas rects
if (lane->idx == 0)
{
/* TODO: Remove this */
/* Create atlas */
Vec2I32 atlas_size = VEC2I32(8192, 8192);
if (G_IsResourceNil(GC.atlas))
{
G_ArenaHandle gpu_arena = G_PermArena();
GC.atlas = G_PushTexture2D(
gpu_arena,
G_Format_R8G8B8A8_Unorm_Srgb,
atlas_size,
/* FIXME: We may need simultaneous access? */
G_Layout_AnyQueue_ShaderRead_CopyRead_CopyWrite_Present,
);
}
for (u64 cmd_idx = 0; cmd_idx < async->cmds.count; ++cmd_idx)
{
GC_Cmd *cmd = &async->cmds.v[cmd_idx];
GC_Glyph *glyph = cmd->glyph;
ResourceKey resource = glyph->desc.font.r;
String resource_data = DataFromResource(resource);
String resource_name = NameFromResource(resource);
GC_GlyphDesc desc = glyph->desc;
TTF_GlyphDesc ttf_desc = TTF_GlyphDescFromCodepoint(desc.codepoint, resource, desc.font_size);
glyph->font_size = ttf_desc.font_size;
glyph->font_ascent = ttf_desc.font_ascent;
glyph->font_descent = ttf_desc.font_descent;
glyph->font_cap = ttf_desc.font_cap;
glyph->advance = ttf_desc.advance;
glyph->bounds = ttf_desc.bounds;
Vec2 dims = DimsFromRng2(glyph->bounds);
Vec2I32 atlas_offset = GC.atlas_pos;
GC.atlas_row_height = MaxI32(GC.atlas_row_height, dims.y);
if (atlas_offset.x + dims.x > atlas_size.x);
{
GC.atlas_pos.x = 0;
GC.atlas_pos.y += GC.atlas_row_height;
GC.atlas_row_height = dims.y;
}
GC.atlas_pos.x += dims.x;
Rng2I32 src_slice = ZI;
src_slice.p0.x = 0;
src_slice.p0.y = 0;
src_slice.p1.x = RoundF32ToI32(dims.x);
src_slice.p1.y = RoundF32ToI32(dims.y);
Vec2I32 src_dims = DimsFromRng2I32(src_slice);
if (src_dims.x > 0 && src_dims.y > 0)
{
u64 src_pixels_count = src_dims.x * src_dims.y;
u32 *src_pixels = PushStructsNoZero(tick->arena, u32, src_pixels_count);
TTF_RasterizeGlyph(src_pixels, src_dims, src_slice, ttf_desc);
DEBUGBREAKABLE;
// G_CommandListHandle cl = G_PrepareCommandList(G_QueueKind_AsyncCopy);
G_CommandListHandle cl = G_PrepareCommandList(G_QueueKind_Direct);
{
G_CopyCpuToTexture(
cl,
GC.atlas, VEC3I32(atlas_offset.x, atlas_offset.y, 0),
src_pixels, VEC3I32(src_dims.x, src_dims.y, 0),
RNG3I32(
VEC3I32(src_slice.p0.x, src_slice.p0.y, 0),
VEC3I32(src_slice.p1.x, src_slice.p1.y, 1)
)
);
}
G_CommitCommandList(cl);
G_SyncCpu(G_QueueMask_All);
}
Atomic32Set(&glyph->ready, 1);
// ResourceKey resource = desc.font.r;
// String resource_data = DataFromResource(resource);
}
}
WaveSync(lane);
//////////////////////////////
//- Process cmds
/* TODO: Process cmds unevenly to account for varying work size */
// if (async->cmds.count > 0)
// {
// RngU64 cmd_idxs = WaveIdxRangeFromCount(lane, async->cmds.count);
// for (u64 cmd_idx = cmd_idxs.min; cmd_idx < cmd_idxs.max; ++cmd_idx)
// {
// GC_Cmd *cmd = &async->cmds.v[cmd_idx];
// GC_Glyph *glyph = cmd->glyph;
// GC_GlyphDesc desc = glyph->desc;
// ResourceKey resource = desc.font.r;
// String resource_data = DataFromResource(resource);
// }
// }
//////////////////////////////
//- End tick
WaveSync(lane);
}

View File

@ -3,38 +3,12 @@
Struct(GC_FontKey)
{
u64 v;
ResourceKey r;
};
////////////////////////////////////////////////////////////
//~ Glyph types
Struct(GC_Glyph)
{
GC_Glyph *next;
u64 hash;
/* Layout info */
Vec2 baseline_offset;
f32 advance;
/* Atlas info */
G_Texture2DRef tex_ref;
Rng2 tex_uv;
Rng2 tex_rect;
/* Font info */
f32 font_size;
f32 font_ascent;
f32 font_descent;
f32 font_cap;
};
Struct(GC_GlyphBin)
{
GC_Glyph *first;
};
Struct(GC_GlyphDesc)
{
GC_FontKey font;
@ -42,6 +16,36 @@ Struct(GC_GlyphDesc)
u32 codepoint;
};
Struct(GC_Glyph)
{
GC_Glyph *next;
GC_GlyphDesc desc;
u64 hash;
Atomic32 ready;
/* Font info */
f32 font_size;
f32 font_ascent;
f32 font_descent;
f32 font_cap;
/* Layout info */
f32 advance;
Rng2 bounds; /* Bounds relative to baseline position */
/* Atlas info */
G_Texture2DRef tex_ref;
Rng2 tex_uv;
Rng2 tex_rect;
};
Struct(GC_GlyphBin)
{
GC_Glyph *first;
};
// Struct(GC_GlyphDescChunk)
// {
// GC_GlyphDescChunk *next;
@ -94,12 +98,10 @@ Struct(GC_Cmd)
GC_Glyph *glyph;
};
Struct(GC_CmdChunk)
Struct(GC_CmdNode)
{
GC_CmdChunk *next;
u64 cmds_count;
GC_Cmd *cmds;
GC_CmdNode *next;
GC_Cmd cmd;
};
////////////////////////////////////////////////////////////
@ -119,13 +121,25 @@ Struct(GC_Ctx)
Mutex glyphs_mutex;
GC_GlyphBin glyph_bins[16384];
Mutex submitted_cmds_mutex;
u64 submitted_cmds_count;
GC_CmdChunk *first_submitted_cmd_chunk;
GC_CmdChunk *last_submitted_cmd_chunk;
/* TODO: Dynamic atlases */
G_ResourceHandle atlas;
G_Texture2DRef atlas_ref;
Vec2I32 atlas_pos;
i32 atlas_row_height;
struct
{
Mutex mutex;
u64 count;
GC_CmdNode *first;
GC_CmdNode *last;
GC_CmdNode *first_free;
} submit;
GC_AsyncCtx async_ctx;
} extern GC;
};
extern GC_Ctx GC;
////////////////////////////////////////////////////////////
//~ Bootstrap

View File

@ -249,7 +249,7 @@ void V_TickForever(WaveLaneCtx *lane)
{
/* TODO: Don't rely on ui report for draw size since it introduces one frame of delay when resizing */
UI_Report vis_rep = UI_ReportFromKey(vis_box);
draw_size = Vec2I32FromVec(DimsFromRng2(vis_rep.screen_rect));
draw_size = RoundVec2ToVec2I32(DimsFromRng2(vis_rep.screen_rect));
}
draw_size.x = MaxI32(draw_size.x, 1);
draw_size.y = MaxI32(draw_size.y, 1);

View File

@ -22,10 +22,10 @@ void PT_RunForever(WaveLaneCtx *lane)
/* Push resources */
Vec2I32 final_target_size = window_frame.draw_size;
G_ResourceHandle final_target = G_PushTexture2D(gpu_frame_arena,
G_Format_R16G16B16A16_Float,
final_target_size,
G_Layout_DirectQueue_ShaderReadWrite,
.flags = G_ResourceFlag_AllowShaderReadWrite);
G_Format_R16G16B16A16_Float,
final_target_size,
G_Layout_DirectQueue_ShaderReadWrite,
.flags = G_ResourceFlag_AllowShaderReadWrite);
/* Push resource handles */
G_Texture2DRef final_target_rhandle = G_PushTexture2DRef(gpu_frame_arena, final_target);

View File

@ -1,27 +1,27 @@
Struct(TTF_Glyph)
////////////////////////////////////////////////////////////
//~ Glyph types
Struct(TTF_GlyphDesc)
{
i32 advance;
Vec2 baseline_offset;
Vec2I32 atlas_p0;
Vec2I32 atlas_p1;
ResourceKey ttf;
u32 codepoint;
f32 advance; /* How far to advance the baseline position */
Rng2 bounds; /* Bounds relative to baseline position */
f32 font_size;
f32 font_ascent;
f32 font_descent;
f32 font_cap;
};
Struct(TTF_Decoded)
{
TTF_Glyph *glyphs;
u16 glyphs_count;
u16 *cache_indices; /* Array of indices into the `glyphs` array in order of `cache_chars` */
u32 image_width;
u32 image_height;
u32 *image_pixels; /* Array of [width * height] pixels */
/* Metrics */
f32 ascent;
f32 descent;
f32 cap;
};
////////////////////////////////////////////////////////////
//~ Bootstrap
void TTF_Bootstrap(void);
TTF_Decoded TTF_Decode(Arena *arena, String encoded, f32 em_size, u32 *cache_codes, u32 cache_codes_count);
////////////////////////////////////////////////////////////
//~ Decode
TTF_GlyphDesc TTF_GlyphDescFromCodepoint(u32 codepoint, ResourceKey ttf, f32 font_size);
void TTF_RasterizeGlyph(u32 *dst, Vec2I32 dst_size, Rng2I32 dst_slice, TTF_GlyphDesc glyph_desc);

File diff suppressed because it is too large Load Diff

View File

@ -140,12 +140,14 @@ static inline UINT32 IDWriteGdiInterop_Release
EXTERN_C HRESULT DECLSPEC_IMPORT WINAPI DWriteCreateFactory (DWRITE_FACTORY_TYPE factoryType, const GUID* iid, void** factory) WIN_NOEXCEPT;
////////////////////////////////////////////////////////////
//~ State types
//~ Context types
/* TODO: Determine font dpi dynamically */
#define TTF_DW_Dpi (96.0f)
Struct(TTF_DW_SharedState)
Struct(TTF_DW_Ctx)
{
struct IDWriteFactory5 *factory;
} extern TTF_DW_shared_state;
IDWriteFactory5 *factory;
};
extern TTF_DW_Ctx TTF_DW;