Fix text rendering: MTLLoadActionLoad + glyph ID path + rif copy
Three critical bugs fixed: 1. MTLLoadActionClear wipes staticTexture every update_begin cycle. Emacs calls update_begin/end multiple times per second (cursor blink, modeline, etc.). Each call was clearing all previously drawn content. Fix: only clear on texture (re)creation; use MTLLoadActionLoad to PRESERVE content between update_begin cycles. Emacs's draw_glyph_string fills backgrounds only for the regions being updated. 2. char2b contains CGGlyph IDs (not Unicode codepoints) in the macfont backend. mtl_draw_glyph_string was calling mtl_cache_glyph() which used CTFontGetGlyphsForCharacters (Unicode→glyph) on values that were already glyph IDs — double conversion, wrong glyphs rendered. Fix: new mtl_cache_glyph_id() uses CTFontDrawGlyphs directly with the glyph ID from char2b, same as ns_draw_glyph_string_foreground does. 3. mtl_patch_terminal_rif replaced the ENTIRE terminal rif (including NULL frame_parm_handlers). init_frame_faces/realize_basic_faces accesses rif->frame_parm_handlers during Fx_create_frame → SIGSEGV. Fix: copy the NS rif (preserving frame_parm_handlers and all management functions), then override ONLY the pixel-drawing functions with Metal. Added: mtl-capture-frame, mtl-draw-stats diagnostics (kept for debugging).
This commit is contained in:
parent
ab0972e901
commit
9c112c768d
3 changed files with 236 additions and 44 deletions
89
src/mtlfns.m
89
src/mtlfns.m
|
|
@ -199,6 +199,81 @@ quads to an off-screen MTLTexture. Returns t on success. */)
|
|||
return mtl_render_text_png (SSDATA (path)) ? Qt : Qnil;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
DEFUN: mtl-capture-frame — save the current Metal staticTexture to PNG.
|
||||
Used for visual regression testing and debugging rendering issues.
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
DEFUN ("mtl-capture-frame", Fmtl_capture_frame, Smtl_capture_frame, 1, 2, 0,
|
||||
doc: /* Save the Metal staticTexture for FRAME (or selected frame) to PATH.
|
||||
Returns t on success. The PNG shows exactly what Metal has rendered into
|
||||
the intermediate texture, before cursor/animation overlay. */)
|
||||
(Lisp_Object path, Lisp_Object frame)
|
||||
{
|
||||
CHECK_STRING (path);
|
||||
struct frame *f = NILP (frame) ? XFRAME (selected_frame) : XFRAME (frame);
|
||||
if (!f) return Qnil;
|
||||
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.staticTexture) return Qnil;
|
||||
|
||||
id<MTLTexture> src = fd.staticTexture;
|
||||
NSUInteger W = src.width, H = src.height;
|
||||
|
||||
id<MTLDevice> dev = mtl_get_device();
|
||||
id<MTLCommandQueue> q = mtl_get_queue();
|
||||
if (!dev || !q) return Qnil;
|
||||
|
||||
/* Blit from Private texture to Shared texture for CPU readback */
|
||||
MTLTextureDescriptor *td =
|
||||
[MTLTextureDescriptor texture2DDescriptorWithPixelFormat:MTLPixelFormatBGRA8Unorm
|
||||
width:W height:H mipmapped:NO];
|
||||
td.usage = MTLTextureUsageShaderRead;
|
||||
td.storageMode = MTLStorageModeShared;
|
||||
id<MTLTexture> readback = [dev newTextureWithDescriptor:td];
|
||||
if (!readback) return Qnil;
|
||||
|
||||
id<MTLCommandBuffer> cmd = [q commandBuffer];
|
||||
id<MTLBlitCommandEncoder> blit = [cmd blitCommandEncoder];
|
||||
[blit copyFromTexture:src
|
||||
sourceSlice:0 sourceLevel:0
|
||||
sourceOrigin:MTLOriginMake(0,0,0)
|
||||
sourceSize:MTLSizeMake(W,H,1)
|
||||
toTexture:readback
|
||||
destinationSlice:0 destinationLevel:0
|
||||
destinationOrigin:MTLOriginMake(0,0,0)];
|
||||
[blit endEncoding];
|
||||
[cmd commit];
|
||||
[cmd waitUntilCompleted];
|
||||
|
||||
/* Read pixels (BGRA) and swap to RGBA for PNG */
|
||||
NSUInteger bpr = W * 4;
|
||||
uint8_t *px = malloc (bpr * H);
|
||||
if (!px) return Qnil;
|
||||
[readback getBytes:px bytesPerRow:bpr
|
||||
fromRegion:MTLRegionMake2D(0,0,W,H) mipmapLevel:0];
|
||||
|
||||
/* BGRA → RGBA */
|
||||
for (NSUInteger i = 0; i < W * H; i++)
|
||||
{ uint8_t b=px[i*4]; px[i*4]=px[i*4+2]; px[i*4+2]=b; }
|
||||
|
||||
CGColorSpaceRef cs = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef ctx = CGBitmapContextCreate(px, W, H, 8, bpr, cs,
|
||||
(CGBitmapInfo)(kCGImageAlphaPremultipliedLast));
|
||||
CGColorSpaceRelease(cs);
|
||||
CGImageRef img = CGBitmapContextCreateImage(ctx);
|
||||
CGContextRelease(ctx);
|
||||
NSString *nspath = [NSString stringWithUTF8String:SSDATA(path)];
|
||||
NSURL *url = [NSURL fileURLWithPath:nspath];
|
||||
CGImageDestinationRef dst = CGImageDestinationCreateWithURL(
|
||||
(__bridge CFURLRef)url, kUTTypePNG, 1, NULL);
|
||||
CGImageDestinationAddImage(dst, img, NULL);
|
||||
bool ok = CGImageDestinationFinalize(dst);
|
||||
CFRelease(dst); CGImageRelease(img); free(px);
|
||||
|
||||
return ok ? Qt : Qnil;
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
DEFUN: mtl-create-frame (stub — use mtl-enable-for-frame instead)
|
||||
----------------------------------------------------------------------- */
|
||||
|
|
@ -284,6 +359,18 @@ DEFUN ("mtl-trail-length", Fmtl_trail_length, Smtl_trail_length, 1, 1, 0,
|
|||
return len;
|
||||
}
|
||||
|
||||
DEFUN ("mtl-draw-stats", Fmtl_draw_stats, Smtl_draw_stats, 0, 0, 0,
|
||||
doc: /* Return diagnostic counters for Metal glyph rendering.
|
||||
Returns an alist with: total-calls, no-fd (no encoder), no-font, glyphs-drawn. */)
|
||||
(void)
|
||||
{
|
||||
return list4 (
|
||||
Fcons (intern ("total-calls"), make_fixnum (mtl_dgs_call_count)),
|
||||
Fcons (intern ("no-fd"), make_fixnum (mtl_dgs_nofd_count)),
|
||||
Fcons (intern ("no-font"), make_fixnum (mtl_dgs_nofont_count)),
|
||||
Fcons (intern ("glyphs-drawn"), make_fixnum (mtl_dgs_drawn_count)));
|
||||
}
|
||||
|
||||
DEFUN ("mtl-animation-status", Fmtl_animation_status, Smtl_animation_status,
|
||||
0, 0, 0,
|
||||
doc: /* Return an alist with current Metal animation configuration. */)
|
||||
|
|
@ -316,6 +403,8 @@ syms_of_mtlfns (void)
|
|||
defsubr (&Smtl_scroll_duration);
|
||||
defsubr (&Smtl_trail_length);
|
||||
defsubr (&Smtl_animation_status);
|
||||
defsubr (&Smtl_capture_frame);
|
||||
defsubr (&Smtl_draw_stats);
|
||||
}
|
||||
|
||||
#endif /* HAVE_MTL */
|
||||
|
|
|
|||
|
|
@ -251,6 +251,16 @@ extern bool mtl_render_offscreen_png (const char *path, int w, int h,
|
|||
extern bool mtl_render_text_png (const char *path);
|
||||
extern void mtl_patch_terminal_rif (struct frame *f);
|
||||
|
||||
/* Device/queue accessors for mtlfns.m (g_device and g_queue are file-static) */
|
||||
extern id<MTLDevice> mtl_get_device (void);
|
||||
extern id<MTLCommandQueue> mtl_get_queue (void);
|
||||
|
||||
/* Diagnostic counters for mtl_draw_glyph_string */
|
||||
extern int mtl_dgs_call_count;
|
||||
extern int mtl_dgs_nofd_count;
|
||||
extern int mtl_dgs_nofont_count;
|
||||
extern int mtl_dgs_drawn_count;
|
||||
|
||||
extern void syms_of_mtlfns (void);
|
||||
|
||||
#endif /* HAVE_MTL */
|
||||
|
|
|
|||
181
src/mtlterm.m
181
src/mtlterm.m
|
|
@ -295,6 +295,12 @@ ns_color_to_pixel (NSColor *c)
|
|||
Global Metal setup (called once)
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
id<MTLDevice>
|
||||
mtl_get_device (void) { return g_device; }
|
||||
|
||||
id<MTLCommandQueue>
|
||||
mtl_get_queue (void) { return g_queue; }
|
||||
|
||||
static BOOL
|
||||
mtl_global_setup (void)
|
||||
{
|
||||
|
|
@ -552,35 +558,23 @@ glyph_cache_insert (uint64_t key)
|
|||
Glyph rasterization — CoreText → R8Unorm atlas patch
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
MtlGlyphCacheEntry *
|
||||
mtl_cache_glyph (CTFontRef font, uint32_t codepoint)
|
||||
/* Core glyph rasterization: rasterize a CGGlyph (ID already resolved) into
|
||||
the atlas. This is the single rasterization path used by BOTH:
|
||||
- mtl_cache_glyph_id (from Emacs glyph_string.char2b — already glyph IDs)
|
||||
- mtl_cache_glyph (from off-screen PNG test, uses Unicode → glyph lookup)
|
||||
|
||||
The critical insight: glyph_string.char2b stores GLYPH IDs for the macfont
|
||||
backend (not Unicode codepoints). Always use glyph IDs with CTFont APIs
|
||||
that accept CGGlyph directly (CTFontGetBoundingRectsForGlyphs, CTFontDrawGlyphs).
|
||||
Never call CTFontGetGlyphsForCharacters on a value that is already a glyph ID.
|
||||
*/
|
||||
static MtlGlyphCacheEntry *
|
||||
mtl_rasterize_glyph_id (CTFontRef font, CGGlyph cgGlyph, uint64_t key)
|
||||
{
|
||||
/* Key: mix font pointer and codepoint */
|
||||
uint64_t key = ((uint64_t)(uintptr_t)font << 21) ^ codepoint;
|
||||
MtlGlyphCacheEntry *entry = glyph_cache_lookup (key);
|
||||
if (entry) return entry;
|
||||
if (!g_atlas) return NULL;
|
||||
|
||||
UniChar chars[2];
|
||||
int nc = 1;
|
||||
if (codepoint >= 0x10000)
|
||||
{
|
||||
/* Surrogate pair */
|
||||
codepoint -= 0x10000;
|
||||
chars[0] = 0xD800 | (codepoint >> 10);
|
||||
chars[1] = 0xDC00 | (codepoint & 0x3FF);
|
||||
nc = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
chars[0] = (UniChar)codepoint;
|
||||
}
|
||||
MtlGlyphCacheEntry *entry;
|
||||
|
||||
CGGlyph cgGlyph = 0;
|
||||
if (!CTFontGetGlyphsForCharacters (font, chars, &cgGlyph, nc) || cgGlyph == 0)
|
||||
return NULL;
|
||||
|
||||
/* Bounding box for this glyph (in font units → pixels) */
|
||||
CGRect bbox = CTFontGetBoundingRectsForGlyphs (font,
|
||||
kCTFontOrientationDefault, &cgGlyph, NULL, 1);
|
||||
|
||||
|
|
@ -657,6 +651,50 @@ mtl_cache_glyph (CTFontRef font, uint32_t codepoint)
|
|||
return entry;
|
||||
}
|
||||
|
||||
/* mtl_cache_glyph_id — PRIMARY path for Emacs text rendering.
|
||||
char2b[i] in glyph_string is a GLYPH ID (CGGlyph), not a Unicode codepoint.
|
||||
We use it directly with CoreText APIs that accept CGGlyph. */
|
||||
static MtlGlyphCacheEntry *
|
||||
mtl_cache_glyph_id (CTFontRef font, CGGlyph glyphId)
|
||||
{
|
||||
if (!font || glyphId == 0) return NULL;
|
||||
|
||||
/* Cache key = (font_ptr × large_prime) XOR glyphId */
|
||||
uint64_t key = ((uint64_t)(uintptr_t)font * 6364136223846793005ULL) ^ (uint64_t)glyphId;
|
||||
|
||||
MtlGlyphCacheEntry *e = glyph_cache_lookup (key);
|
||||
if (e) return e;
|
||||
if (!g_atlas) return NULL;
|
||||
|
||||
return mtl_rasterize_glyph_id (font, glyphId, key);
|
||||
}
|
||||
|
||||
/* mtl_cache_glyph — SECONDARY path for off-screen test renders.
|
||||
Accepts a Unicode codepoint, converts to glyph ID first. */
|
||||
MtlGlyphCacheEntry *
|
||||
mtl_cache_glyph (CTFontRef font, uint32_t codepoint)
|
||||
{
|
||||
if (!font) return NULL;
|
||||
|
||||
UniChar chars[2];
|
||||
int nc = 1;
|
||||
if (codepoint >= 0x10000)
|
||||
{
|
||||
codepoint -= 0x10000;
|
||||
chars[0] = 0xD800 | (codepoint >> 10);
|
||||
chars[1] = 0xDC00 | (codepoint & 0x3FF);
|
||||
nc = 2;
|
||||
}
|
||||
else
|
||||
chars[0] = (UniChar)codepoint;
|
||||
|
||||
CGGlyph cgGlyph = 0;
|
||||
if (!CTFontGetGlyphsForCharacters (font, chars, &cgGlyph, nc) || cgGlyph == 0)
|
||||
return NULL;
|
||||
|
||||
return mtl_cache_glyph_id (font, cgGlyph);
|
||||
}
|
||||
|
||||
/* -----------------------------------------------------------------------
|
||||
Per-frame Metal state (stored as ObjC associated object on EmacsView)
|
||||
----------------------------------------------------------------------- */
|
||||
|
|
@ -931,9 +969,16 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
|
||||
- (void)beginFrame
|
||||
{
|
||||
/* Ensure staticTexture exists and matches drawable size */
|
||||
CGSize dsz = self.metalLayer.drawableSize;
|
||||
|
||||
/* Ensure staticTexture exists and matches drawable size.
|
||||
Bug fix: multiple update_begin/end cycles happen per Emacs redisplay pass
|
||||
(cursor blink, modeline, etc.). Using MTLLoadActionClear on every beginFrame
|
||||
wiped out the drawing from the PREVIOUS cycle, leaving only the background.
|
||||
Fix: only clear when the texture is (re)created; preserve content otherwise.
|
||||
Emacs's draw_glyph_string already fills backgrounds for updated regions. */
|
||||
NSUInteger tw = (NSUInteger)dsz.width, th = (NSUInteger)dsz.height;
|
||||
BOOL needsClear = NO;
|
||||
if (!self.staticTexture
|
||||
|| self.staticTexture.width != tw
|
||||
|| self.staticTexture.height != th)
|
||||
|
|
@ -944,6 +989,7 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
td.usage = MTLTextureUsageRenderTarget | MTLTextureUsageShaderRead;
|
||||
td.storageMode = MTLStorageModePrivate;
|
||||
self.staticTexture = [g_device newTextureWithDescriptor:td];
|
||||
needsClear = YES; /* New or resized texture: clear to background color */
|
||||
}
|
||||
|
||||
NSSize sz = self.metalLayer.frame.size;
|
||||
|
|
@ -957,9 +1003,11 @@ easing_apply (MtlScrollEasing mode, float t)
|
|||
unpack_color (bg, &r, &g, &b);
|
||||
|
||||
MTLRenderPassDescriptor *rpd = [MTLRenderPassDescriptor renderPassDescriptor];
|
||||
rpd.colorAttachments[0].texture = self.staticTexture;
|
||||
rpd.colorAttachments[0].loadAction = MTLLoadActionClear;
|
||||
rpd.colorAttachments[0].clearColor = MTLClearColorMake(r, g, b, 1.0);
|
||||
rpd.colorAttachments[0].texture = self.staticTexture;
|
||||
/* Load previous content (not clear) unless this is a fresh texture.
|
||||
The NS backend redraws only dirty regions; our Metal backend must do the same. */
|
||||
rpd.colorAttachments[0].loadAction = needsClear ? MTLLoadActionClear : MTLLoadActionLoad;
|
||||
rpd.colorAttachments[0].clearColor = MTLClearColorMake(r, g, b, 1.0);
|
||||
rpd.colorAttachments[0].storeAction = MTLStoreActionStore;
|
||||
|
||||
self.cmdBuf = [g_queue commandBuffer];
|
||||
|
|
@ -1568,12 +1616,19 @@ mtl_draw_image_texture (MtlFrameData *fd, id<MTLTexture> tex,
|
|||
redisplay_interface — all file-static, called by xdisp.c engine
|
||||
----------------------------------------------------------------------- */
|
||||
|
||||
int mtl_dgs_call_count = 0; /* total calls */
|
||||
int mtl_dgs_nofd_count = 0; /* no fd or encoder */
|
||||
int mtl_dgs_nofont_count = 0; /* no ctfont */
|
||||
int mtl_dgs_drawn_count = 0; /* glyphs actually drawn */
|
||||
|
||||
static void
|
||||
mtl_draw_glyph_string (struct glyph_string *s)
|
||||
{
|
||||
mtl_dgs_call_count++;
|
||||
|
||||
struct frame *f = s->f;
|
||||
MtlFrameData *fd = mtl_get_frame_data (f);
|
||||
if (!fd || !fd.encoder) return;
|
||||
if (!fd || !fd.encoder) { mtl_dgs_nofd_count++; return; }
|
||||
|
||||
struct face *face = s->face;
|
||||
unsigned long fg = face ? face->foreground : 0xECEFF4;
|
||||
|
|
@ -1617,26 +1672,27 @@ mtl_draw_glyph_string (struct glyph_string *s)
|
|||
|
||||
/* Get CoreText font */
|
||||
CTFontRef ctfont = mtl_ctfont_for_face (face);
|
||||
if (!ctfont) return;
|
||||
if (!ctfont) { mtl_dgs_nofont_count++; return; }
|
||||
|
||||
|
||||
/* Draw each glyph at its correct x position.
|
||||
We use text_extents to get individual advance widths. */
|
||||
float pen_x = (float)s->x;
|
||||
float baseline_y = (float)s->ybase;
|
||||
|
||||
for (int i = 0; i < s->nchars; i++)
|
||||
{
|
||||
unsigned int cp = s->char2b ? s->char2b[i] : 0;
|
||||
if (!cp) { pen_x += (float)FRAME_COLUMN_WIDTH (f); continue; }
|
||||
/* char2b contains GLYPH IDs for the macfont backend — NOT Unicode codepoints.
|
||||
Use mtl_cache_glyph_id which calls CoreText with the ID directly. */
|
||||
CGGlyph glyphId = s->char2b ? (CGGlyph)s->char2b[i] : 0;
|
||||
if (!glyphId) { pen_x += (float)FRAME_COLUMN_WIDTH (f); continue; }
|
||||
|
||||
MtlGlyphCacheEntry *ge = mtl_cache_glyph (ctfont, cp);
|
||||
MtlGlyphCacheEntry *ge = mtl_cache_glyph_id (ctfont, glyphId);
|
||||
if (!ge) continue;
|
||||
|
||||
/* Glyph origin: (pen_x, baseline_y).
|
||||
The cache entry's bearing_y is distance from top-of-bitmap to baseline.
|
||||
So the top of the bitmap is baseline_y - bearing_y (in top-left coords). */
|
||||
if (ge->width > 0)
|
||||
{
|
||||
mtl_dgs_drawn_count++;
|
||||
/* bearing_y: distance from glyph top-left to baseline.
|
||||
In our top-left coord system, glyph top = baseline_y - bearing_y. */
|
||||
CGPoint origin = CGPointMake (pen_x, baseline_y);
|
||||
[fd drawGlyph:ge at:origin color:fg];
|
||||
}
|
||||
|
|
@ -1956,17 +2012,54 @@ static const char mtl_resize_obs_key;
|
|||
|
||||
@end
|
||||
|
||||
/* One copy of the NS rif, patched with our Metal drawing functions.
|
||||
Allocated once when mtl_patch_terminal_rif is first called.
|
||||
CRITICAL: We must NOT replace the entire rif — the NS rif's
|
||||
frame_parm_handlers, produce_glyphs, and management functions are
|
||||
called by init_frame_faces / realize_basic_faces / Fx_create_frame.
|
||||
Replacing them all with NULLs causes SIGSEGV there.
|
||||
Solution: copy the NS rif, then override ONLY the drawing functions. */
|
||||
static struct redisplay_interface *mtl_ns_rif_copy = NULL;
|
||||
|
||||
void
|
||||
mtl_patch_terminal_rif (struct frame *f)
|
||||
{
|
||||
struct terminal *term = FRAME_TERMINAL (f);
|
||||
if (!term) return;
|
||||
if (!term || !term->rif) return;
|
||||
|
||||
/* Replace the redisplay interface so draw_glyph_string etc. use Metal. */
|
||||
term->rif = &mtl_redisplay_interface;
|
||||
/* Build a patched copy of the NS rif (once per process lifetime).
|
||||
All NS management/frame functions are preserved; only pixel-drawing
|
||||
functions are replaced with Metal equivalents. */
|
||||
if (!mtl_ns_rif_copy)
|
||||
{
|
||||
mtl_ns_rif_copy = xmalloc (sizeof (struct redisplay_interface));
|
||||
*mtl_ns_rif_copy = *term->rif; /* copy all NS functions as baseline */
|
||||
|
||||
/* Patch render cycle hooks so Metal manages the frame lifecycle.
|
||||
NS update_begin (lockFocus) and update_end (flushWindow) are replaced. */
|
||||
/* Override only the functions that perform pixel drawing.
|
||||
Everything else (frame_parm_handlers, produce_glyphs, etc.)
|
||||
stays as the NS implementation — frame management must work. */
|
||||
mtl_ns_rif_copy->scroll_run_hook = mtl_scroll_run;
|
||||
mtl_ns_rif_copy->after_update_window_line_hook = mtl_after_update_window_line;
|
||||
mtl_ns_rif_copy->flush_display = mtl_flush_display;
|
||||
mtl_ns_rif_copy->draw_fringe_bitmap = mtl_draw_fringe_bitmap;
|
||||
mtl_ns_rif_copy->define_fringe_bitmap = mtl_define_fringe_bitmap;
|
||||
mtl_ns_rif_copy->destroy_fringe_bitmap = mtl_destroy_fringe_bitmap;
|
||||
mtl_ns_rif_copy->compute_glyph_string_overhangs= mtl_compute_glyph_string_overhangs;
|
||||
mtl_ns_rif_copy->draw_glyph_string = mtl_draw_glyph_string;
|
||||
mtl_ns_rif_copy->clear_frame_area = mtl_clear_frame_area;
|
||||
mtl_ns_rif_copy->clear_under_internal_border = mtl_clear_under_internal_border;
|
||||
mtl_ns_rif_copy->draw_window_cursor = mtl_draw_window_cursor;
|
||||
mtl_ns_rif_copy->draw_vertical_window_border = mtl_draw_vertical_window_border;
|
||||
mtl_ns_rif_copy->draw_window_divider = mtl_draw_window_divider;
|
||||
mtl_ns_rif_copy->shift_glyphs_for_insert = mtl_shift_glyphs_for_insert;
|
||||
/* show_hourglass, hide_hourglass, default_font_parameter: keep NS versions */
|
||||
}
|
||||
|
||||
term->rif = mtl_ns_rif_copy;
|
||||
|
||||
/* Patch render cycle hooks so Metal manages the frame pixel lifecycle.
|
||||
The NS backend's update_begin calls [view lockFocus] for CoreGraphics;
|
||||
we bypass that entirely and use Metal command buffers instead. */
|
||||
term->update_begin_hook = mtl_update_begin;
|
||||
term->update_end_hook = mtl_update_end;
|
||||
term->clear_frame_hook = mtl_clear_frame;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue