I have a Metal-based application that utilizes AVFoundation for movie playback + seeking. To start, I am only processing .mov files and nothing else, and even the app in question will not process any other format. While it has been working effectively in the past, I recently received feedback from some M1 users about black frames only showing up on their app regardless of which time they set their seek bar to.
I have performed the following troubleshooting in my attempts find the root source for this black texture bug:
Verified that the video being processed is .mov movie file type.
Verified that the CVPixelBufferRef object returned from AVPlayerItemVideoOutput's -copyPixelBufferForItemTime: is valid, i.e. not nil.
Verified that the MTLTexture created from the CVPixelBufferRef is also valid, i.e. also not nil.
Converted the MTLTexture to a bitmap and saved it as a .JPEG image to the user's disk.
The last part is probably the most important step here as the images saved are also all black (for users experiencing the bug) when viewed using Finder, and made me come to the assumption that I might be using AVFoundation somewhat wrong. While I hope that my opening post might not be too long in regards to code, below are the following steps I am performing in order to process videos to be rendered using Metal for your reference.
Inside my reader class, I have the following properties:
VideoReaderClass.m
#property (retain) AVPlayer *vidPlayer;
#property (retain) AVPlayerItem *vidPlayerItem;
#property (retain) AVPlayerItemVideoOutput *playerItemVideoOutput;
#property (retain) AVMutableComposition *videoMutableComp;
#property (assign) AVPlayerItemStatus playerItemStatus;
// The frame duration of the composition, as well as the media being processed.
#property (assign) CMTime frameDuration;
// Tracks the time to insert a new footage to
#property (assign) CMTime startInsertTime;
// Weak reference to a delegate responsible for processing seek:
#property (weak) id<VideoReaderRenderer> vidReaderRenderer;
A method called before reading starts. Handles observer cleanup as well.
- (void)initializeReadMediaPipeline
{
[_lock lock];
_startInsertTime = kCMTimeZero;
_playerItemStatus = AVPlayerItemStatusUnknown;
if(_videoMutableComp)
{
[_videoMutableComp release];
}
_videoMutableComp = [AVMutableComposition composition];
[_videoMutableComp retain];
if(_vidPlayer)
{
[[_vidPlayer currentItem] removeObserver:self
forKeyPath:#"status"
context:MyAddAVPlayerItemKVOContext];
[[_vidPlayer currentItem] removeObserver:self
forKeyPath:#"playbackBufferFull"
context:MyAVPlayerItemBufferFullKVOContext];
[[_vidPlayer currentItem] removeObserver:self
forKeyPath:#"playbackBufferEmpty"
context:MyAVPlayerItemBufferEmptyKVOContext];
[[_vidPlayer currentItem] removeObserver:self
forKeyPath:#"playbackLikelyToKeepUp"
context:MyAVPlayerItemBufferKeepUpKVOContext];
[_vidPlayer release];
_vidPlayer = nil;
}
[_lock unlock];
}
In this class is a public method to be called in a background queue when the app should begin processing a video, or a set of videos.
- (BOOL)addMediaAtLocation:(NSURL *)location
{
BOOL result = NO;
[_lock lock];
NSDictionary* optsInfo =
#{
AVURLAssetPreferPreciseDurationAndTimingKey : #(YES)
};
AVURLAsset* assetURL = [AVURLAsset URLAssetWithURL:location options:optsInfo];
AVAssetTrack* assetTrack = [assetURL tracksWithMediaType:AVMediaTypeVideo].firstObject;
NSError* error = nil;
[_videoMutableComp insertTimeRange:CMTimeRangeMake(kCMTimeZero, assetTrack.timeRange.duration)
ofAsset:assetURL
atTime:_startInsertTime
error:&error];
if(!error)
{
[_vidPlayerItem addObserver:self
forKeyPath:#"status"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:MyAddAVPlayerItemKVOContext];
[_vidPlayerItem addObserver:self
forKeyPath:#"playbackBufferFull"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:MyAVPlayerItemBufferFullKVOContext];
[_vidPlayerItem addObserver:self
forKeyPath:#"playbackBufferEmpty"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:MyAVPlayerItemBufferEmptyKVOContext];
[_vidPlayerItem addObserver:self
forKeyPath:#"playbackLikelyToKeepUp"
options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
context:MyAVPlayerItemBufferKeepUpKVOContext];
[_vidPlayer replaceCurrentItemWithPlayerItem:_vidPlayerItem];
}
_vidPlayer = [[AVPlayer alloc] init];
if(_playerItemVideoOutput)
{
[_playerItemVideoOutput release];
_playerItemVideoOutput = nil;
}
[_lock unlock];
}
Called externally by our view controller when seeking is needed.
- (void)seekToFrame:(CMTime)time
{
if(_vidReaderRenderer)
{
if(_vidPlayerItem && _playerItemVideoOutput)
{
[_vidPlayerItem seekTimeTime:time
toleranceBefore:kCMTimeZero
toleranceAfter:kCMTimeZero
completionHandler:^(BOOL finished){
if(finished)
{
CVPixelBufferRef p_buffer = [_playerItemVideoOutput copyPixelBufferForItemTime:time itemTimeForDisplay:nil];
if(p_buffer)
{
[_vidReaderRenderer seekOperationFinished:p_buffer];
}
}
}];
}
}
}
Lastly, this is where I handle the notification when the AVPlayerItem's status is set for "ReadyToPlay."
- (void)observeValueForKeyPath:(NSString *)keyPath
ofObject:(id)object
change:(NSDictionary<NSKeyValueChangeKey,id> *)change
context:(void *)context
{
// ... Checking for right contexts here, omitting for this example.
if([keyPath isEqualToString:#"status"])
{
AVPlayerItemStatus status = AVPlayerItemStatusUnknown;
// Get the status change from the change dictionary
NSNumber *statusNumber = change[NSKeyValueChangeNewKey];
if([statusNumber isKindOfClass:[NSNumber class]])
{
status = statusNumber.integerValue;
}
// Switch over the status
switch(status)
{
case AVPlayerItemStatusReadyToPlay:
{
// Ready to Play
if(_vidPlayerItem)
{
[_lock lock];
NSDictionary* attribs =
#{
(NSString*)kCVPixelBufferPixelFormatTypeKey : #(kCVPixelFormatType_64RGBAHalf)
};
_playerItemVideoOutput = [[AVPlayerItemVideoOutput alloc] initWithPixelBufferAttributes:attribs];
[_vidPlayerItem addOutput:_playerItemVideoOutput];
[_vidPlayer setRate:0.0];
_playerItemStatus = status;
[_lock unlock];
// "Wake up" the AVPlayer/AVPlayerItem here.
[self seekToFrame:kCMTimeZero];
}
break;
}
}
}
}
The code listing below is the class that also acts the delegate for a custom protocol called VideoReaderRenderer, which handles the seekToTime: completion block, as well as converting the pixel buffer to a MTLTexture:
RendererDelegate.m
At some point in its initialization and before performing any seek operations, I instantiate the CVMetalTextureCacheRef.
CVReturn ret_val = CVMetalTextureCacheCreate(kCFAllocatorDefault, nil, _mtlDevice, nil, &_metalTextureCache);
The method in which my VideoReader class calls inside seekToTime's completion block:
-(void)seekOperationFinished:(CVPixelBufferRef)pixelBuffer
{
CVMetalTextureRef mtl_tex = NULL;
size_t w = CVPixelBufferGetWidth(pixelBuffer);
size_t h = CVPixelBufferGetHeight(pixelBuffer);
CVPixelBufferLockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
CVReturn ret_val = CVMetalTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_metalTextureCache,
pixelBuffer,
nil,
MTLPixelFormatRGBA16Float,
w,
h,
0,
&mtl_tex);
CVPixelBufferUnlockBaseAddress(pixelBuffer, kCVPixelBufferLock_ReadOnly);
if(ret_val != kCVReturnSuccess)
{
if(mtl_tex != NULL)
{
CVBufferRelease(mtl_tex);
CVPixelBufferRelease(pixelBuffer);
}
return;
}
id<MTLTexture> inputTexture = CVMetalTextureGetTexture(mtl_tex);
if(!inputTexture)
return;
NSSize texSize = NSMakeSize(inputTexture.width, inputTexture.height);
_viewPortSize = (simd_uint2){(uint)texSize.width, (uint)texSize.height};
// Create the texture here.
[_textureLock lock];
if(NSEqualSizes(_projectSize, texSize))
{
if(!_inputFrameTex)
{
MTLTextureDescriptor* texDescriptor = [MTLTextureDescriptor new];
texDescriptor.width = texSize.width;
texDescriptor.height = texSize.height;
texDescriptor.pixelFormat = MTLPixelFormatRGBA16Float;
texDescriptor.usage = MTLTextureUsageShaderWrite | MTLTextureUsageShaderRead;
_inputFrameTex = [_mtlDevice newTextureWithDescriptor:texDescriptor];
}
id<MTLCommandBuffer> commandBuffer = [_drawCopyCommandQueue commandBuffer];
id<MTLBlitCommandEncoder> blitEncoder = [commandBuffer blitCommandEncoder];
[blitEncoder copyFromTexture:inputTexture
sourceSlice:0
sourceLevel:0
sourceOrigin:MTLOriginMake(0, 0, 0)
sourceSize:MTLSizeMake(inputTexture.width, inputTexture.height, 1)
toTexture:_inputFrameTex
destinationSlice:0
destinationLevel:0
destinationOrigin:MTLOriginMake(0, 0, 0)];
[blitEncoder endEncoding];
[commandBuffer commit];
[commandBuffer waitUntilCompleted];
}
[_textureLock unlock];
CVBufferRelease(mtl_tex);
CVPixelBufferRelease(pixelBuffer);
// Added "trouble-shooting" code to save the contents of the MTLTexture as a JPEG onto the user's disk.
if(_inputFrameTex)
{
// ... Save contents of texture to disk as JPEG.
}
}
Once again, my apologies for the rather long post. Additional details include having our rendering being displayed on a custom NSView backed by a CAMetalLayer, where its subsequent draw calls are called by a CVDisplayLink for background rendering. Though I don't think the latter seems to be the source of the problem of seekToTime: returning black frames. Can anyone care to shed some light to my situation? Thank you very much in advance.
Related
I am trying to create a non-Document-based application for Mac OS X that randomizes cards for the game of Dominion.
From many of the ones I have tried, the only thing I cannot seem to do is limit the number of sets picked from a selection made by the user, and things worked pretty well in my program, but I am having issues.
I am trying to get the results to print in a custom view, but every time I look at the print preview, nothing shows, except header text, as specified in an NSMutableString.
This piece of code is what is being used to print and is found in MasterViewController:
- (IBAction)print:(id)sender
{
NSMutableString *content = [[NSMutableString alloc] initWithString:#"Cards\r\n\r\n"];
for (int i = 0; i < [supply.game count]; i++)
{
[content appendFormat:#"Card: %# Set: %# Cost: %d\r\n", [supply.game[i] name], [supply.game[i] collection], [supply.game[i] cost]];
}
[content appendFormat:#"\r\n\r\nRequired\r\n\r\n"];
for (int i = 0; i < [[setup supply] count]; i++)
{
NSDictionary* current = [setup supply][i];
NSString* key = [current allKeys][0]; // get the key of the current dictionary must be 0, as there is only one key
int value = [[current valueForKey:key] integerValue]; // variable to hold key value
if (value > 0) {
[content appendFormat:#"%#: %#", key, #"Yes"];
}
else
{
[content appendFormat:#"%#: %#", key, #"No"];
}
}
printView.content = [NSMutableString stringWithString:content];
[printView print:sender];
}
the data initially gets filled into some tableviews, which displays the correct content, and the supply.game array is the exact array that contains cards used for games.
setup is a property that refers to a view controller that populates a table with kinds of cards that may be required for games (e.g. shelters, colonies, ruins, spoils, and potions) and the supply method is supposed to return the array that view controller creates, which is itself not empty, as that table populates properly.
printView is a property that is assigned to a custom view found in MainMenu.xib and is the real view being used to print from.
the printView class looks like this:
header:
#import <Cocoa/Cocoa.h>
#interface PrintView : NSView
{
NSMutableString* content;
}
#property NSMutableString* content;
- (void)drawStringInRect:(NSRect)rect; // method to draw string to page
- (void)print:(id)sender; // method to print
#end
implementation:
#import "PrintView.h"
#implementation PrintView
#synthesize content;
- (BOOL)acceptsFirstResponder
{
return YES;
}
- (void)print:(id)sender
{
[[NSPrintOperation printOperationWithView:self] runOperation];
}
- (void)drawRect:(NSRect)dirtyRect {
NSGraphicsContext *context = [NSGraphicsContext currentContext];
if ([context isDrawingToScreen])
{
}
else
{
[[NSColor whiteColor] set];
NSRect bounds = [self bounds];
if (content == nil || [content length] == 0)
{
NSRectFill(bounds);
}
else
{
[self drawStringInRect:bounds];
}
}
}
- (void)drawStringInRect:(NSRect)rect
{
NSSize strSize; // variable to hold string size
NSPoint strOrigin; // variable used to position text
NSMutableDictionary *attributes = [[NSMutableDictionary alloc] init];
[attributes setObject:[NSFont fontWithName:#"Helvetica" size:12] forKey:NSFontAttributeName];
[attributes setObject:[NSColor blackColor] forKey:NSForegroundColorAttributeName];
strSize = [content sizeWithAttributes:attributes];
strOrigin.x = rect.origin.x + (rect.size.width - strSize.width)/2;
strOrigin.y = rect.origin.y + (rect.size.height - strSize.height)/2;
[content drawAtPoint:strOrigin withAttributes:attributes];
}
#end
When I check the array sizes for printing operation, the size of the arrays is reported as zero, thus resulting in my current problem
If you need more code, here is code from Github, but I do not have the experimental branch up there, which is where the above code came from, though it should not be too different.
The MasterViewController will show how the supply.game array is made and SetupViewController houses the code that is used to determine what is needed in the game, as well as show how the supply array from [setup supply] is being produced.
MasterViewController has also been added as an object to MainMenu.xib, so I do not know if that affects anything.
Any idea of what I need to do?
Edit: Added in info that might be relevant
I am able to add NSImages to my NSCollectionView without having to first save them on disk. The images are fed into the collection view from an NSMutableArray. This way people can see the images without first having to save them.
Is there something similar that I can achieve with IKImageBrowserView? NSCollectionView is functional when it comes to representing images, but I would like to see if I can do something similar with IKImageBrowserView.
I can easily implement IKImageBrowserView with images saved on disk (Apple docs cover how this works) but can't figure out exactly where to look or how to go about adding images to the browser view directly from NSMutableArray instead of first saving them images to disk.
I'm at a loss here. Apart from the docs, I'm not really sure where else to look for direction. Or what to even call what I'm looking to do.
EDIT: (Here's some of the code)
// The data object -- if it is possible to represent an image object, this is where I am probably going wrong.
#interface ImageObject : NSObject
#property (readwrite, copy) NSImage *image;
#property (readwrite, copy) NSString *imageID;
- (id)initWithImage:(NSImage *)anImage;
- (NSString *)imageUID;
- (NSString *)imageRepresentationType;
- (id)imageRepresentation;
#end
#implementation ImageObject
#synthesize image = _image;
#synthesize imageID = _imageID;
- (id)initWithImage:(NSImage *)anImage
{
if (self = [super init]) {
_image = [anImage copy];
}
return self;
}
- (NSString *)imageUID
{
return _imageID;
}
- (NSString *)imageRepresentationType
{
return IKImageBrowserNSImageRepresentationType;
}
- (id)imageRepresentation
{
return _image;
}
#end
// This is how objects are supposed to be added to the browserView. All of this is straight from Apple.
- (void)updateDatasource
{
[_browserImages addObjectsFromArray:_importedImages];
[_importedImages removeAllObjects];
[imageBrowser reloadData];
}
- (NSUInteger)numberOfItemsInImageBrowser:(IKImageBrowserView *)aBrowser
{
return [_browserImages count];
}
- (id)imageBrowser:(IKImageBrowserView *)aBrowser itemAtIndex:(NSUInteger)index
{
return [_browserImages objectAtIndex:index];
}
This is where I try to add NSImages to the browserView but nothing happens. The array gets populated (which means the images are generated without any errors) but nothing happens on the screen.
AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:[oPanel URL] options:nil];
NSMutableArray *timesArray = [self generateTimeForSpecifiedNumberOfFramesInVideo:10 UsingAsset:asset];
self.imageGenerator = [AVAssetImageGenerator assetImageGeneratorWithAsset:asset];
[[self imageGenerator] generateCGImagesAsynchronouslyForTimes:timesArray completionHandler:^(CMTime requestedTime, CGImageRef image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError *error) {
NSImage *testImage = [[NSImage alloc] initWithCGImage:image size:NSZeroSize];
if (result == AVAssetImageGeneratorSucceeded) {
ImageObject *objects = [[ImageObject alloc] initWithImage:testImage];
[_importedImages addObject:objects];
}
}
As for exploring the rest of the search results...been there done that. If I did miss anything, kindly mark this question as duplicate indicating what post already existed where this issue has been addressed.
EDIT:
I have accepted the answer below. Along with the unique IDs problem. I had overlooked a simple thing which was the requirement to call the updateDatasource method.
The most important point of using IKImageBrowser is create a unique image ID for each element. The following is an example. In fact, it comes from the project that I'm currently working on. I have just implemented IKImageBrowser in it. The code below assumes that you have 36 images (Image01.png, Image02.png..., Image36.png) imported into the project.
// .h
#interface AppDelegate : NSObject {
IBOutlet IKImageBrowserView *browserView;
NSMutableArray *imageArray;
}
// .m
#import "IKBBrowserItem.h"
#import <QuartzCore/QuartzCore.h>
- (void)applicationWillFinishLaunching:(NSNotification *)notification {
imageArray = [[NSMutableArray alloc]init];
}
- (void)populateImage {
for (NSInteger i2 = 1; i2 <= 36 ; i2++) {
NSString *name;
if (i2 < 10) {
name = [NSString stringWithFormat:#"Image0%ld",(long)i2];
} else {
name = [NSString stringWithFormat:#"Image%ld",(long)i2];
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
NSImage *Image0 = [NSImage imageNamed:name];
NSInteger ran = [self genRandom:1000000:9999999];
NSString *imageID = [NSString stringWithFormat:#"%#%li",name,ran];
IKBBrowserItem *item = [[IKBBrowserItem alloc] initWithImage:Image0 imageID:imageID:name];
[imageArray addObject:item];
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
}
[browserView reloadData];
}
- (NSInteger)genRandom: (NSInteger)min :(NSInteger)max {
int num1;
do {
num1 = arc4random() % max;
} while (num1 < min);
return num1;
}
You don't need to use a random integer generator (genRandom) above, but just make sure that no imageID is the same.
This web site has a sample project, which should get you going. (I have no affiliation.) So make sure you download and run it. Then take a closer look and improve it for your needs.
First of all, I'm an Objective-C novice. So I'm not very familiar with OS X or iOS development. My experience is mostly in Java.
I'm creating an agent-based modeling-framework. I'd like to display the simulations and to do that I'm writing a little application. First, a little bit about the framework. The framework has a World class, in which there is a start method, which iterates over all agents and has them perform their tasks. At the end of one "step" of the world (i.e., after all the agents have done their thing), the start method calls the intercept method of an object that implements InterceptorProtocol. This object was previously passed in via the constructor. Using the interceptor, anyone can get a hook into the state of the world. This is useful for logging, or in the scenario that I'm trying to accomplish: displaying the information in a graphical manner. The call to intercept is synchronous.
Now as far as the GUI app is concerned, it is pretty simple. I have a controller that initializes a custom view. This custom view also implements InterceptorProtocol so that it can listen in, to what happens in the world. I create a World object and pass in the view as an interceptor. The view maintains a reference to the world through a private property and so once I have initialized the world, I set the view's world property to the world I have just created (I realize that this creates a cycle, but I need a reference to the world in the drawRect method of the view and the only way I can have it is if I maintain a reference to it from the class).
Since the world's start method is synchronous, I don't start the world up immediately. In the drawRect method I check to see if the world is running. If it is not, I start it up in a background thread. If it is, I examine the world and display all the graphics that I need to.
In the intercept method (which gets called from start running on the background thread), I set setNeedsToDisplay to YES. Since the start method of the world is running in a separate thread, I also have a lock object that I use to synchronize so that I'm not working on the World object while it's being mutated (this part is kind of janky and it's probably not working the way I expect it to - there are more than a few rough spots and I'm simply trying to get a little bit working; I plan to clean up later).
My problem is that the view renders some stuff, and then it pretty much locks up. I can see that the NSLog statements are being called and so the code is running, but nothing is getting updated on the view.
Here's some of the pertinent code:
MasterViewController
#import "MasterViewController.h"
#import "World.h"
#import "InfectableBug.h"
#interface MasterViewController ()
#end
#implementation MasterViewController
- (id)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibBundleOrNil
{
self = [super initWithNibName:nibNameOrNil bundle:nibBundleOrNil];
if (self) {
_worldView = [[WorldView alloc] init];
World* world = [[World alloc] initWithName: #"Bhumi"
rows: 100
columns: 100
iterations: 2000
snapshotInterval: 1
interceptor: _worldView];
for(int i = 0; i < 999; i++) {
NSMutableString* name = [NSMutableString stringWithString: #"HealthyBug"];
[name appendString: [[NSNumber numberWithInt: i] stringValue]];
[world addBug: [[InfectableBug alloc] initWithWorld: world
name: name
layer: #"FirstLayer"
infected: NO
infectionRadius: 1
incubationPeriod: 10
infectionStartIteration: 0]];
}
NSLog(#"Added all bugs. Going to add infected");
[world addBug: [[InfectableBug alloc] initWithWorld: world
name: #"InfectedBug"
layer: #"FirstLayer"
infected: YES
infectionRadius: 1
incubationPeriod: 10
infectionStartIteration: 0]];
[_worldView setWorld: world];
//[world start];
}
return self;
}
- (NSView*) view {
return self.worldView;
}
#end
WorldView
#import "WorldView.h"
#import "World.h"
#import "InfectableBug.h"
#implementation WorldView
#synthesize world;
- (id) initWithFrame:(NSRect) frame {
self = [super initWithFrame:frame];
if (self) {
// Initialization code here.
}
return self;
}
- (void) drawRect:(NSRect) dirtyRect {
CGContextRef myContext = [[NSGraphicsContext currentContext] graphicsPort];
CGContextClearRect(myContext, CGRectMake(0, 0, 1024, 768));
NSUInteger rows = [world rows];
NSUInteger columns = [world columns];
NSUInteger cellWidth = 1024 / columns;
NSUInteger cellHeight = 768 / rows;
if([world running]) {
#synchronized (_lock) {
//Ideally we would need layers, but for now let's just get this to display
NSArray* bugs = [world bugs];
NSEnumerator* enumerator = [bugs objectEnumerator];
InfectableBug* bug;
while ((bug = [enumerator nextObject])) {
if([bug infected] == YES) {
CGContextSetRGBFillColor(myContext, 128, 0, 0, 1);
} else {
CGContextSetRGBFillColor(myContext, 0, 0, 128, 1);
}
NSLog(#"Drawing bug %# at %lu, %lu with width %lu and height %lu", [bug name], [bug x] * cellWidth, [bug y] * cellHeight, cellWidth, cellHeight);
CGContextFillRect(myContext, CGRectMake([bug x] * cellWidth, [bug y] * cellHeight, cellWidth, cellHeight));
}
}
} else {
[world performSelectorInBackground: #selector(start) withObject: nil];
}
}
- (BOOL) isFlipped {
return YES;
}
- (void) intercept: (World *) aWorld {
struct timespec time;
time.tv_sec = 0;
time.tv_nsec = 500000000L;
//nanosleep(&time, NULL);
#synchronized (_lock) {
[self setNeedsDisplay: YES];
}
}
#end
start method in World.m:
- (void) start {
running = YES;
while(currentIteration < iterations) {
#autoreleasepool {
[bugs shuffle];
NSEnumerator* bugEnumerator = [bugs objectEnumerator];
Bug* bug;
while((bug = [bugEnumerator nextObject])) {
NSString* originalLayer = [bug layer];
NSUInteger originalX = [bug x];
NSUInteger originalY = [bug y];
//NSLog(#"Bug %# is going to act and location %i:%i is %#", [bug name], [bug x], [bug y], [self isOccupied: [bug layer] x: [bug x] y: [bug y]] ? #"occupied" : #"not occupied");
[bug act];
//NSLog(#"Bug has acted");
if(![originalLayer isEqualToString: [bug layer]] || originalX != [bug x] || originalY != [bug y]) {
//NSLog(#"Bug has moved");
[self moveBugFrom: originalLayer atX: originalX atY: originalY toLayer: [bug layer] atX: [bug x] atY: [bug y]];
//NSLog(#"Updated bug position");
}
}
if(currentIteration % snapshotInterval == 0) {
[interceptor intercept: self];
}
currentIteration++;
}
}
//NSLog(#"Done.");
}
Please let me know if you'd like to see any other code. I realize that the code is not pretty; I was just trying to get stuff to work and I plan on cleaning it up later. Also, if I'm violating an Objective-C best practices, please let me know!
Stepping out for a bit; sorry if I don't respond immediately!
Whew, quiet a question for probably a simple answer: ;)
UI updates have to be performed on the main thread
If I read your code correctly, you call the start method on a background thread. The start method contains stuff like moveBugFrom:... and also the intercept: method. The intercept method thus calls setNeedsDisplay: on a background thread.
Have all UI related stuff perform on the main thread. Your best bet is to use Grand Central Dispatch, unless you need to support iOS < 4 or OS X < 10.6 (or was it 10.7?), like this:
dispatch_async(dispatch_get_main_queue(), ^{
// perform UI updates
});
I've just started with the sparrow framework, and have been following "The Big Sparrow Tutorial" by Gamua themselves. I'm on the first part of the tutorial, using the AppScaffold 1.3 but when I try to compile my basic code it hangs at the loading screen and gives me a SIGABRT error.
I put an exception breakpoint, and it stopped here, in GameController.m (seen at bottom) of the AppScaffold:
mGame = [[Game alloc] initWithWidth:gameWidth height:gameHeight];
This was also my only output:
2012-07-30 07:19:54.787 AppScaffold[1682:10a03] -[Game initWithWidth:height:]: unrecognized selector sent to instance 0x7553980
(lldb)
I am using the stock AppScaffold, the only thing I changed was the Game.m.
This is my Game.m:
#interface Game : SPSprite
#end
#implementation Game
{
#private
SPImage *mBackground;
SPImage *mBasket;
NSMutableArray *mEggs;
}
- (id)init
{
if((self = [super init]))
{
//load the background image first, add it to the display tree
//and keep it for later use
mBackground = [[SPImage alloc] initWithContentsOfFile:#"background.png"];
[self addChild:mBackground];
//load the image of the basket, add it to the display tree
//and keep it for later use
mBasket = [[SPImage alloc] initWithContentsOfFile:#"basket.png"];
[self addChild:mBasket];
//create a list that will hold the eggs,
//which we will add and remove repeatedly during the game
mEggs = [[NSMutableArray alloc] init];
}
return self;
}
- (void)dealloc
{
[mBackground release];
[mBasket release];
[mEggs release];
[super dealloc];
}
#end
I've tried my best to use my basic troubleshooting tactics, but I'm very new to Obj-C and Sparrow and could use a hand :)
Thanks
EDIT: I've addded the GameController.m contents here for clarity:
//
// GameController.m
// AppScaffold
//
#import <OpenGLES/ES1/gl.h>
#import "GameController.h"
#interface GameController ()
- (UIInterfaceOrientation)initialInterfaceOrientation;
#end
#implementation GameController
- (id)initWithWidth:(float)width height:(float)height
{
if ((self = [super initWithWidth:width height:height]))
{
float gameWidth = width;
float gameHeight = height;
// if we start up in landscape mode, width and height are swapped.
UIInterfaceOrientation orientation = [self initialInterfaceOrientation];
if (UIInterfaceOrientationIsLandscape(orientation)) SP_SWAP(gameWidth, gameHeight, float);
mGame = [[Game alloc] initWithWidth:gameWidth height:gameHeight];
mGame.pivotX = gameWidth / 2;
mGame.pivotY = gameHeight / 2;
mGame.x = width / 2;
mGame.y = height / 2;
[self rotateToInterfaceOrientation:orientation animationTime:0];
[self addChild:mGame];
}
return self;
}
- (void)dealloc
{
[mGame release];
[super dealloc];
}
- (UIInterfaceOrientation)initialInterfaceOrientation
{
// In an iPhone app, the 'statusBarOrientation' has the correct value on Startup;
// unfortunately, that's not the case for an iPad app (for whatever reason). Thus, we read the
// value from the app's plist file.
NSDictionary *bundleInfo = [[NSBundle mainBundle] infoDictionary];
NSString *initialOrientation = [bundleInfo objectForKey:#"UIInterfaceOrientation"];
if (initialOrientation)
{
if ([initialOrientation isEqualToString:#"UIInterfaceOrientationPortrait"])
return UIInterfaceOrientationPortrait;
else if ([initialOrientation isEqualToString:#"UIInterfaceOrientationPortraitUpsideDown"])
return UIInterfaceOrientationPortraitUpsideDown;
else if ([initialOrientation isEqualToString:#"UIInterfaceOrientationLandscapeLeft"])
return UIInterfaceOrientationLandscapeLeft;
else
return UIInterfaceOrientationLandscapeRight;
}
else
{
return [[UIApplication sharedApplication] statusBarOrientation];
}
}
- (void)rotateToInterfaceOrientation:(UIInterfaceOrientation)interfaceOrientation
animationTime:(double)animationTime
{
float angles[] = {0.0f, 0.0f, -PI, PI_HALF, -PI_HALF};
float oldAngle = mGame.rotation;
float newAngle = angles[(int)interfaceOrientation];
// make sure that rotation is always carried out via the minimal angle
while (oldAngle - newAngle > PI) newAngle += TWO_PI;
while (oldAngle - newAngle < -PI) newAngle -= TWO_PI;
// rotate game
if (animationTime)
{
SPTween *tween = [SPTween tweenWithTarget:mGame time:animationTime
transition:SP_TRANSITION_EASE_IN_OUT];
[tween animateProperty:#"rotation" targetValue:newAngle];
[[SPStage mainStage].juggler removeObjectsWithTarget:mGame];
[[SPStage mainStage].juggler addObject:tween];
}
else
{
mGame.rotation = newAngle;
}
// inform all display objects about the new game size
BOOL isPortrait = UIInterfaceOrientationIsPortrait(interfaceOrientation);
float newWidth = isPortrait ? MIN(mGame.gameWidth, mGame.gameHeight) :
MAX(mGame.gameWidth, mGame.gameHeight);
float newHeight = isPortrait ? MAX(mGame.gameWidth, mGame.gameHeight) :
MIN(mGame.gameWidth, mGame.gameHeight);
if (newWidth != mGame.gameWidth)
{
mGame.gameWidth = newWidth;
mGame.gameHeight = newHeight;
SPEvent *resizeEvent = [[SPResizeEvent alloc] initWithType:SP_EVENT_TYPE_RESIZE
width:newWidth height:newHeight animationTime:animationTime];
[mGame broadcastEvent:resizeEvent];
[resizeEvent release];
}
}
// Enable this method for the simplest possible universal app support: it will display a black
// border around the iPhone (640x960) game when it is started on the iPad (768x1024); no need to
// modify any coordinates.
/*
- (void)render:(SPRenderSupport *)support
{
if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad)
{
glEnable(GL_SCISSOR_TEST);
glScissor(64, 32, 640, 960);
[super render:support];
glDisable(GL_SCISSOR_TEST);
}
else
[super render:support];
}
*/
#end
Here is my Xcode project: http://cl.ly/2e3g02260N47
You are calling a
initWithWidth:height:
method, while none is defined in your class.
From your edit, it seems that the initWithWidth method is declared in the class GameController, not in Game.
So, it seems that the
In which context are you calling initWithWidth:height: method is declared in Game.h but you define it in GameController.m.
This explains both why you get the SIGABRT and the errors when compiling.
The fix is calling
mGame = [[GameController alloc] init];
from GameController initWithWidth...
- (id)initWithWidth:(float)width height:(float)height
{
if ((self = [super initWithWidth:width height:height]))
{
float gameWidth = width;
float gameHeight = height;
// if we start up in landscape mode, width and height are swapped.
UIInterfaceOrientation orientation = [self initialInterfaceOrientation];
if (UIInterfaceOrientationIsLandscape(orientation)) SP_SWAP(gameWidth, gameHeight, float);
mGame = [[Game alloc] init];
mGame.pivotX = gameWidth / 2;
mGame.pivotY = gameHeight / 2;
mGame.x = width / 2;
mGame.y = height / 2;
[self rotateToInterfaceOrientation:orientation animationTime:0];
[self addChild:mGame];
}
return self;
}
The tutorial was very old and was incompatible with the latest scaffold;
I did this:
- (id)init
{
if((self = [super init]))
{
when I should've done this:
- (id)initWithWidth:(float)width height:(float)height
{
if ((self = [super initWithWidth:width height:height]))
{
thanks, though sergio!
(There are much better sparrow tutorials and I'm even making my own video tutorials :P)
I'm working on an iPad app that has a slider that is used to scroll through data. When scrolling, a map is displayed and the data is updated. The problem is, if you scroll fast enough (or somehow trigger the race condition), the app crashes on accessing a zombie NSString. I've been able to track it down in the Profiler and found this:
Event Type RefCt Timestamp Size Responsible Library Responsible Caller
Malloc 1 01:55.166.466 16 Foundation -[NSPlaceholderString initWithFormat:locale:arguments:]
Autorelease <null> 01:55.166.472 0 Foundation +[NSString stringWithFormat:]
CFRetain 2 01:55.166.473 0 My Program -[StateView updateVotes:]
CFRetain 3 01:55.166.476 0 UIKit -[UILabel setText:]
CFRelease 2 01:55.166.504 0 My Program -[StateView updateVotes:]
CFRelease 1 01:55.177.661 0 Foundation -[NSAutoreleasePool release]
CFRelease 0 01:55.439.090 0 UIKit -[UILabel setText:]
Zombie -1 01:55.439.109 0 UIKit -[NSString(UIStringDrawing) drawAtPoint:forWidth:withFont:lineBreakMode:letterSpacing:includeEmoji:]
I'm using ARC on iOS5, so I'm not in control of the retain/release at all. Even if I was, looking at the above, it is correct. The problem seems to be a race condition between the drawing function and the UILabel string actually changing. The UILabel releases the first string, as a new one has been set, but the drawing function is holding a reference to it somehow, but did not retain it.
As a note, I have not modified the UILabel in any way.
Any ideas?
--- Code added as update:
Slider update:
-(void)sliderValueChanged:(UISlider *)slider {
float position = slider.value - 1790.0f;
int year;
if(position <= 0.0f) {
year = 1789;
} else {
year = 1792 + (floor(position / 4.0f)*4);
}
[self setYear:year];
}
setYear:
-(void)setYear:(int)year {
if (year == currentYear) {
// year didn't change, so don't do anything
return;
}
[yearLabel setText:[[NSString alloc] initWithFormat:#"%i", year]];
currentYear = year;
[self getMapForYear:year];
}
getMapForYear:
-(void) getMapForYear:(int)year {
[self setToMap:[historicalData objectForKey:[NSNumber numberWithInt:year]];
}
setToMap:
-(void) setToMap:(HistoricalMap *)map {
// Label the map
for (State *state in [map states]) {
[mapController setVotes:[state votes] forState:[state abbreviation]];
}
}
setVotes:forState:
-(void)setVotes:(NSNumber *)votes forState:(NSString *)stateAbbreviation {
StateView *state = [states objectForKey:stateAbbreviation];
if (state == nil) {
NSLog(#"Invalid State Votes -- %#", stateAbbreviation);
return;
}
[state updateVotes:votes];
[state setNeedsDisplay];
}
updateVotes:
-(void)updateVotes:(NSNumber *)newVotes {
[self setVotes:newVotes];
NSString *voteString = [[NSString alloc] initWithFormat:#"%#", newVotes];
[voteLabel setText:voteString];
if ([newVotes isEqual:[NSNumber numberWithInt:0]]) {
[[self voteLabel] setHidden:YES];
[[self stateAbbreviationLabel] setHidden:YES];
} else {
[[self stateAbbreviationLabel] setHidden:NO];
[[self voteLabel] setHidden:NO];
}
}
I think you are attempting to do far too much during the slider's movement. Creating and executing core data fetch requests alone would seem to be overkill, let alone updating the entire GUI and a screenful of labels. Have you tested the performance of this on a device?
It could be worth profiling these sections of code and seeing where the time is spent. You could look at caching the fetch requests or the results, for example, or you may have to resort to updating only when the slider has stopped, or only for every n increments along the path.
You havw several memory-leaks with NSString:
[yearLabel setText:[[NSString alloc] initWithFormat:#"%i", year]]; // leak
Create string with stringWithFormat method instead
[yearLabel setText:[NSString stringWithFormat:#"%i", year]];
[NSString stringWithFormat: **is the best way formatting the string than any other..**