Skip to content

Commit 141d2a6

Browse files
committed
camera/avfoundation: cache per-frame scratch buffers; quiet per-frame logs
captureOutput:didOutputSampleBuffer:fromConnection: allocated and freed four full-frame scratch buffers on every callback: an intermediate colour-converted image, a rotated image, an optional mirrored image, and a scaled image. For a 720p BGRA camera at 30 fps that was roughly 3.5 MB × 4 allocations × 30 Hz of allocator churn on the main thread (the delegate queue is dispatch_get_main_queue() - see setSampleBufferDelegate:queue: in setupCameraSession). Cache the buffers on the AVCameraManager singleton via private ivars in a class extension. Grow on demand (realloc only when the required size exceeds the current capacity), otherwise reuse. Camera source resolution is effectively constant for the life of a session, so after the first frame no allocator work happens. Scratch buffers are freed in avfoundation_free via a new teardownScratchBuffers method, so a driver teardown/re-init with a different target resolution does not leak stale capacity. The original code swapped rotatedBuffer.data = mirroredBuffer on successful mirror; with stable per-frame buffer identities that trick no longer fits. Replace it with a local 'scaleSource' pointer that selects between the rotated and mirrored buffer, preserving the same fallback semantics (mirror failure scales from the rotated buffer). Also silence two per-frame RARCH_LOG statements (the aspect-fill scaling dimensions and the 270-degree rotation notice) by putting them behind #ifdef DEBUG. They duplicate information already logged once at init and produced one log line per camera frame in release builds. ARC / MRC compatibility: the file is compiled under MRC in the top- level Makefile and the RetroArch*.xcodeproj / RetroArch_OSX107 / iOS13 projects, and under ARC in the iOS6-11 projects and the iOS Theos build. The new code uses only plain-C pointers for the scratch buffers (invisible to ARC's memory management), no explicit retain/release/autorelease calls, and a method name (teardownScratchBuffers) that avoids the release/init/copy/new/ mutableCopy selector families that ARC treats specially. No -dealloc override is added - it would require [super dealloc] under MRC (banned under ARC), and the singleton is never dealloc'd in practice; teardownScratchBuffers from avfoundation_free is the real cleanup path. Thread-safety: unchanged. The capture delegate is invoked on the main queue and the consumer of frameBuffer reads on the main thread as well; no new synchronization introduced. The scratch-buffer realloc happens in the same callback that uses them.
1 parent 93449d3 commit 141d2a6

1 file changed

Lines changed: 108 additions & 50 deletions

File tree

camera/drivers/avfoundation.m

Lines changed: 108 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,31 @@ @interface AVCameraManager : NSObject <AVCaptureVideoDataOutputSampleBufferDeleg
4545
@property (assign) size_t height;
4646

4747
- (bool)setupCameraSession;
48+
- (void)teardownScratchBuffers;
49+
@end
50+
51+
/* Private class extension: cached per-frame scratch buffers.
52+
* captureOutput:didOutputSampleBuffer: used to malloc + free four full-
53+
* frame buffers per callback (intermediate colour-converted, rotated,
54+
* optional mirrored, and scaled). For a typical 720p BGRA camera that
55+
* was ~3.5 MB × 4 allocations × 30 fps = hundreds of MB/s of allocator
56+
* churn on the main thread (the capture delegate queue is the main
57+
* queue — see setSampleBufferDelegate:queue: below). Cache the
58+
* buffers on the manager and grow only when the required size
59+
* exceeds the current capacity; free them in teardownScratchBuffers
60+
* on driver teardown. Ivars are plain C pointers so the file remains
61+
* identically correct under MRC and ARC. */
62+
@interface AVCameraManager ()
63+
{
64+
void *_intermediateBuf;
65+
size_t _intermediateCap;
66+
void *_rotatedBuf;
67+
size_t _rotatedCap;
68+
void *_mirroredBuf;
69+
size_t _mirroredCap;
70+
void *_scaledBuf;
71+
size_t _scaledCap;
72+
}
4873
@end
4974

5075
@implementation AVCameraManager
@@ -123,13 +148,21 @@ - (void)captureOutput:(AVCaptureOutput *)output
123148
#ifdef DEBUG
124149
RARCH_LOG("[Camera] Processing frame %zux%zu format: %u.\n", sourceWidth, sourceHeight, (unsigned int)pixelFormat);
125150
#endif
126-
// Create intermediate buffer for full-size converted image
127-
uint32_t *intermediateBuffer = (uint32_t*)malloc(sourceWidth * sourceHeight * 4);
128-
if (!intermediateBuffer) {
129-
RARCH_ERR("[Camera] Failed to allocate intermediate buffer.\n");
130-
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
131-
return;
151+
// Intermediate buffer for full-size converted image. Cached on
152+
// the manager - grown only when the required size exceeds the
153+
// current capacity. See class extension above for rationale.
154+
size_t intermediateSize = sourceWidth * sourceHeight * 4;
155+
if (intermediateSize > _intermediateCap) {
156+
void *tmp = realloc(_intermediateBuf, intermediateSize);
157+
if (!tmp) {
158+
RARCH_ERR("[Camera] Failed to allocate intermediate buffer.\n");
159+
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
160+
return;
161+
}
162+
_intermediateBuf = tmp;
163+
_intermediateCap = intermediateSize;
132164
}
165+
uint32_t *intermediateBuffer = (uint32_t*)_intermediateBuf;
133166

134167
vImage_Buffer srcBuffer = {}, intermediateVBuffer = {}, dstBuffer = {};
135168
vImage_Error err = kvImageNoError;
@@ -201,14 +234,12 @@ - (void)captureOutput:(AVCaptureOutput *)output
201234

202235
default:
203236
RARCH_ERR("[Camera] Unsupported pixel format: %u.\n", (unsigned int)pixelFormat);
204-
free(intermediateBuffer);
205237
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
206238
return;
207239
}
208240

209241
if (err != kvImageNoError) {
210242
RARCH_ERR("[Camera] Error converting color format: %ld.\n", err);
211-
free(intermediateBuffer);
212243
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
213244
return;
214245
}
@@ -232,20 +263,27 @@ - (void)captureOutput:(AVCaptureOutput *)output
232263
// TODO: Add an API to retroarch to allow for mirroring of front camera
233264
shouldMirror = true; // Mirror front camera
234265
#endif
266+
#ifdef DEBUG
235267
RARCH_LOG("[Camera] Using 270-degree rotation with mirroring for front camera in portrait mode.\n");
268+
#endif
236269
}
237270
}
238271
#endif
239272

240-
// Rotate image
241-
vImage_Buffer rotatedBuffer = {};
242-
rotatedBuffer.data = malloc(sourceWidth * sourceHeight * 4);
243-
if (!rotatedBuffer.data) {
244-
RARCH_ERR("[Camera] Failed to allocate rotation buffer.\n");
245-
free(intermediateBuffer);
246-
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
247-
return;
273+
// Rotate image (cached scratch buffer)
274+
size_t rotatedSize = sourceWidth * sourceHeight * 4;
275+
if (rotatedSize > _rotatedCap) {
276+
void *tmp = realloc(_rotatedBuf, rotatedSize);
277+
if (!tmp) {
278+
RARCH_ERR("[Camera] Failed to allocate rotation buffer.\n");
279+
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
280+
return;
281+
}
282+
_rotatedBuf = tmp;
283+
_rotatedCap = rotatedSize;
248284
}
285+
vImage_Buffer rotatedBuffer = {};
286+
rotatedBuffer.data = _rotatedBuf;
249287

250288
// Set dimensions based on rotation angle
251289
if (rotationDegrees == 90 || rotationDegrees == 270) {
@@ -267,42 +305,46 @@ - (void)captureOutput:(AVCaptureOutput *)output
267305

268306
if (err != kvImageNoError) {
269307
RARCH_ERR("[Camera] Error rotating image: %ld.\n", err);
270-
free(rotatedBuffer.data);
271-
free(intermediateBuffer);
272308
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
273309
return;
274310
}
275311

276-
// Mirror the image if needed
312+
// Mirror the image if needed. On success, the scale step below
313+
// reads from the mirrored buffer; on failure it falls back to
314+
// rotated. Unlike the original code we don't swap pointers —
315+
// each scratch has a stable identity across frames.
316+
vImage_Buffer *scaleSource = &rotatedBuffer;
317+
vImage_Buffer mirroredBuffer = {};
318+
277319
if (shouldMirror) {
278-
vImage_Buffer mirroredBuffer = {};
279-
mirroredBuffer.data = malloc(rotatedBuffer.height * rotatedBuffer.rowBytes);
280-
if (!mirroredBuffer.data) {
281-
RARCH_ERR("[Camera] Failed to allocate mirror buffer.\n");
282-
free(rotatedBuffer.data);
283-
free(intermediateBuffer);
284-
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
285-
return;
320+
size_t mirroredSize = rotatedBuffer.height * rotatedBuffer.rowBytes;
321+
if (mirroredSize > _mirroredCap) {
322+
void *tmp = realloc(_mirroredBuf, mirroredSize);
323+
if (!tmp) {
324+
RARCH_ERR("[Camera] Failed to allocate mirror buffer.\n");
325+
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
326+
return;
327+
}
328+
_mirroredBuf = tmp;
329+
_mirroredCap = mirroredSize;
286330
}
287-
331+
mirroredBuffer.data = _mirroredBuf;
288332
mirroredBuffer.width = rotatedBuffer.width;
289333
mirroredBuffer.height = rotatedBuffer.height;
290334
mirroredBuffer.rowBytes = rotatedBuffer.rowBytes;
291335

292336
err = vImageHorizontalReflect_ARGB8888(&rotatedBuffer, &mirroredBuffer, kvImageNoFlags);
293337

294338
if (err == kvImageNoError) {
295-
// Free rotated buffer and use mirrored buffer for scaling
296-
free(rotatedBuffer.data);
297-
rotatedBuffer = mirroredBuffer;
339+
scaleSource = &mirroredBuffer;
298340
} else {
299341
RARCH_ERR("[Camera] Error mirroring image: %ld.\n", err);
300-
free(mirroredBuffer.data);
342+
/* scaleSource stays pointed at rotatedBuffer */
301343
}
302344
}
303345

304346
// Calculate aspect fill scaling
305-
float sourceAspect = (float)rotatedBuffer.width / rotatedBuffer.height;
347+
float sourceAspect = (float)scaleSource->width / scaleSource->height;
306348
float targetAspect = (float)self.width / self.height;
307349

308350
vImage_Buffer scaledBuffer = {};
@@ -318,30 +360,32 @@ - (void)captureOutput:(AVCaptureOutput *)output
318360
scaledHeight = (size_t)(self.width / sourceAspect);
319361
}
320362

363+
#ifdef DEBUG
321364
RARCH_LOG("[Camera] Aspect fill scaling from %zux%zu to %zux%zu.\n",
322-
rotatedBuffer.width, rotatedBuffer.height, scaledWidth, scaledHeight);
365+
scaleSource->width, scaleSource->height, scaledWidth, scaledHeight);
366+
#endif
323367

324-
scaledBuffer.data = malloc(scaledWidth * scaledHeight * 4);
325-
if (!scaledBuffer.data) {
326-
RARCH_ERR("[Camera] Failed to allocate scaled buffer.\n");
327-
free(rotatedBuffer.data);
328-
free(intermediateBuffer);
329-
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
330-
return;
368+
size_t scaledSize = scaledWidth * scaledHeight * 4;
369+
if (scaledSize > _scaledCap) {
370+
void *tmp = realloc(_scaledBuf, scaledSize);
371+
if (!tmp) {
372+
RARCH_ERR("[Camera] Failed to allocate scaled buffer.\n");
373+
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
374+
return;
375+
}
376+
_scaledBuf = tmp;
377+
_scaledCap = scaledSize;
331378
}
332-
379+
scaledBuffer.data = _scaledBuf;
333380
scaledBuffer.width = scaledWidth;
334381
scaledBuffer.height = scaledHeight;
335382
scaledBuffer.rowBytes = scaledWidth * 4;
336383

337384
// Scale maintaining aspect ratio
338-
err = vImageScale_ARGB8888(&rotatedBuffer, &scaledBuffer, NULL, kvImageHighQualityResampling);
385+
err = vImageScale_ARGB8888(scaleSource, &scaledBuffer, NULL, kvImageHighQualityResampling);
339386

340387
if (err != kvImageNoError) {
341388
RARCH_ERR("[Camera] Error scaling image: %ld.\n", err);
342-
free(scaledBuffer.data);
343-
free(rotatedBuffer.data);
344-
free(intermediateBuffer);
345389
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
346390
return;
347391
}
@@ -360,14 +404,22 @@ - (void)captureOutput:(AVCaptureOutput *)output
360404
self.width * 4);
361405
}
362406

363-
// Clean up
364-
free(scaledBuffer.data);
365-
free(rotatedBuffer.data);
366-
free(intermediateBuffer);
407+
/* Scratch buffers retained on the manager; freed on teardown. */
367408
CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
368409
} // End of autorelease pool
369410
}
370411

412+
- (void)teardownScratchBuffers {
413+
if (_intermediateBuf) { free(_intermediateBuf); _intermediateBuf = NULL; }
414+
_intermediateCap = 0;
415+
if (_rotatedBuf) { free(_rotatedBuf); _rotatedBuf = NULL; }
416+
_rotatedCap = 0;
417+
if (_mirroredBuf) { free(_mirroredBuf); _mirroredBuf = NULL; }
418+
_mirroredCap = 0;
419+
if (_scaledBuf) { free(_scaledBuf); _scaledBuf = NULL; }
420+
_scaledCap = 0;
421+
}
422+
371423
- (AVCaptureDevice *)selectCameraDevice {
372424
RARCH_LOG("[Camera] Selecting camera device...\n");
373425

@@ -630,6 +682,12 @@ static void avfoundation_free(void *data)
630682
avf->manager.frameBuffer = NULL;
631683
}
632684

685+
/* The manager is a singleton; its scratch buffers from the per-
686+
* frame conversion/rotation/mirror/scale pipeline persist across
687+
* driver instances. Free them here so a subsequent init starts
688+
* clean and stale capacity from a prior session does not linger. */
689+
[avf->manager teardownScratchBuffers];
690+
633691
free(avf);
634692
RARCH_LOG("[Camera] AVFoundation camera freed.\n");
635693
}

0 commit comments

Comments
 (0)