Dealing with Stage3D Error 3702 ~ July 23, 2014

The Update (2014-07-24)

Today, I updated my Starling to the latest on github, and discovered what I wish I’d known a week ago: this solution has been in place in Starling proper for a couple of months. Still, I went to the trouble of typing it all up and explaining the problem. This solution would still be useful for people using other Stage3D libs, so I’m going to leave it up. Just be aware that if you’re using Starling, update to the latest and you don’t need to worry about this issue!

The Problem

Stage3D’s Error #3702 is the “Context3D not available” error that may occur if you try to initialize Stage3D with an unsupported profile. In Starling, this error is typically handled by Starling itself, and results in a dark red warning across the stage with a message like “Context 3D not available! Possible reasons: wrong wmode or missing device support.” If you’re like me, this error never happens on your own machines or devices, but you start getting complaints from players that they can’t play your game, and you have no idea why.

I ended up adding an automated bug report to my web build when this particular case comes up to see just how many people are affected, and I was shocked by how quickly I landed a pile of bug reports. I don’t have specific player affected percentage numbers, but it turned out not to be the very rare case that I had previously assumed it is, so I promptly freaked out and flailed around for a few hours, trying to get that flood of reports down to a trickle. It was happening across all modern browsers, OSs and Flash versions; there seemed to be no rhyme or reason.

The Flailing

I did a lot of googling, and found that some people were encountering this problem, but did not find a good general case solution. A lot of the posts I found spread around the Internet showed a general lack of understanding and confusion about why some people were affected and not others. I finally came across a post on the Rebuild game forums with the same problem. Rebuild is a fantastic series of games built by the incredible Sarah Northway. The forum post looked like she’d probably found a solution. It wasn’t posted there, so I bugged her on twitter and she generously shared her erudite wisdom with me. The solution found here is hers, adapted a bit.

Context3D Profiles

The first thing to understand about this error is what the various Context3DProfiles are, and why you might want to use each one.

A quick (by no means comprehensive) breakdown:

The Counterfeit Solution

Adobe recently added Stage3D.requestContext3DMatchingProfiles which Starling added support for by passing a Vector.<String> of profiles in for the profile parameter of the Starling constructor. This was meant to automate this process, correctly degrading through profiles until one is found that will work on the current computer or device. Unfortunately, Adobe appears to have flubbed it; this method no longer appears to support Context3DRenderMode.AUTO, which is the mechanism that lets Stage3D init fall back to software rendering if it must. This can happen if the computer doesn’t have hardware acceleration support, or (more likely) the player has turned off hardware acceleration for the Flash Player.

The result is that if you try to use the new requestContext3DMatchingProfiles method, you can end up landing a nasty Error #3702, even if you passed in all of the Context3D profiles.

Code Time

The solution that Sarah (and now also Mosaic Medley) uses is to attempt intialization of each profile in order using the older Stage3D.requestContext3D function, catching errors and moving on to the next until a working Context3DProfile is found. With appropriate embed settings (wmode=direct), and requiring a modern Flash version (12.0.0 is good enough for my uses), I have yet to receive an Error #3702 using this approach.

The crux is in how we initialize Stage3D:

App.as
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
    /**
     * Initializes the first available Stage3D with the best Context3D it can support. Will fall
     * back to software rendering in Context3DProfile.BASELINE_CONSTRAINED if necessary.
     *
     * @return a Future that may succeed with the profile in use by the newly initialized
     *         Context3D.
     */
    protected function initStage3D () :Future {
        var stage3D :Stage3D = stage.stage3Ds[0];

        // try all three profiles in descending order of complexity
        var profiles :Vector.<String> = new <String>[ Context3DProfile.BASELINE_EXTENDED,
            Context3DProfile.BASELINE, Context3DProfile.BASELINE_CONSTRAINED ];
        var currentProfile :String;
        stage3D.addEventListener(flash.events.Event.CONTEXT3D_CREATE, onCreated);
        stage3D.addEventListener(ErrorEvent.ERROR, onError);
        var promise :Promise = new Promise();

        function requestNextProfile () :void {
            // pull off the next profile and try to init Stage3D with it
            currentProfile = profiles.shift();

            try {
                stage3D.requestContext3D(Context3DRenderMode.AUTO, currentProfile);
            } catch (e :Error) {
                if (profiles.length > 0) {
                    // try again next frame
                    setTimeout(requestNextProfile, 1);
                } else throw e;
            }
        }

        function onCreated (e :flash.events.Event) :void {
            var isSoftwareMode :Boolean =
                stage3D.context3D.driverInfo.toLowerCase().indexOf("software") >= 0;
            if (isSoftwareMode && profiles.length > 0) {
                // don't settle for software mode if there are more hardware profiles to try
                setTimeout(requestNextProfile, 1);
                return;
            }

            // We've found a working profile. Clean up and exit.
            cleanup();
            promise.succeed(currentProfile);
        }

        function onError (e :ErrorEvent) :void {
            // if we have another profile to try, try it; else fail for good
            if (profiles.length > 0) setTimeout(requestNextProfile, 1);
            else {
                cleanup();
                promise.fail(e);
            }
        }

        function cleanup () :void {
            stage3D.removeEventListener(flash.events.Event.CONTEXT3D_CREATE, onCreated);
            stage3D.removeEventListener(ErrorEvent.ERROR, onError);
        }

        // kick off the process by requesting the first profile.
        requestNextProfile();
        return promise;
    }

Note that I’m using a Future here. This is from the reactive programming library for actionscript, React, which I cannot recommend enough. Promise is simply the concrete implementation of Future.

The rest of the implementation is pretty straightforward. We start with a class that extends Sprite, and start the Stage3D init when we’re added to the stage:

App.as
18
19
20
21
22
23
24
25
26
27
28
public class App extends flash.display.Sprite {
    public function App () {
        addEventListener(Event.ADDED_TO_STAGE, addedToStage);
    }

    protected function addedToStage (e :flash.events.Event) :void {
        initStage3D()
            .onFailure(function (e :ErrorEvent) :void {
                trace("Stage 3D error [" + e.toString() + "]");
            }).onSuccess(initStarling);
    }

initStarling is given the profile String by the Future that initStage3D returns. I found that on some platforms, Stage3D.context3D.profile was mysteriously missing. I didn’t find a reason for it, and didn’t dig further because I was able to just pass it around myself. Perhaps this problem is also why Starling requires that you pass the configured profile in to the constructor.

The last bit to understand is that when we pass the already initialized Stage3D instance in to Starling, it will assume that it’s sharing that Stage3D with another framework, which modifies its behavior in several ways. Since we’re not actually sharing the Stage3D around, we can reset the shareContext property on the Starling instance before continuing with init:

App.as
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
    protected function initStarling (profile :String) :void {
        var stage3D :Stage3D = stage.stage3Ds[0];
        trace("Stage 3D init complete [" + profile + ", " + stage3D.context3D.driverInfo + "]");

        var starlingInst :Starling = new Starling(starling.display.Sprite, stage, null, stage3D,
            Context3DRenderMode.AUTO, profile);
        // we're not actually sharing context, but because we passed Starling a stage3D that is
        // already initialized, it thinks we are.
        starlingInst.shareContext = false;
        starlingInst.addEventListener(starling.events.Event.ROOT_CREATED, run);
        starlingInst.start();
    }

    protected function run (e :starling.events.Event) :void {
        var starlingInst :Starling = Starling(e.target);
        starlingInst.removeEventListener(starling.events.Event.ROOT_CREATED, run);

        // Starling is good to go, let the game building commence!
    }

That’s it! Hopefully this can clear up some of the general Error #3702 confusion out there. If you have any questions, comments or find a problem with the code here, leave it in a comment below.


comments powered by Disqus