summaryrefslogtreecommitdiff
path: root/indra/newview/llfloater360capture.cpp
blob: a0890b7f9d01074db539e6467f0c43184206f315 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
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
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
/**
 * @file llfloater360capture.cpp
 * @author Callum Prentice (callum@lindenlab.com)
 * @brief Floater code for the 360 Capture feature
 *
 * $LicenseInfo:firstyear=2011&license=viewerlgpl$
 * Second Life Viewer Source Code
 * Copyright (C) 2011, Linden Research, Inc.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation;
 * version 2.1 of the License only.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Linden Research, Inc., 945 Battery Street, San Francisco, CA  94111  USA
 * $/LicenseInfo$
 */

#include "llviewerprecompiledheaders.h"

#include "llfloater360capture.h"

#include "llagent.h"
#include "llagentui.h"
#include "llbase64.h"
#include "llcallbacklist.h"
#include "llenvironment.h"
#include "llimagejpeg.h"
#include "llmediactrl.h"
#include "llradiogroup.h"
#include "llslurl.h"
#include "lltextbox.h"
#include "lltrans.h"
#include "lluictrlfactory.h"
#include "llversioninfo.h"
#include "llviewercamera.h"
#include "llviewercontrol.h"
#include "llviewerpartsim.h"
#include "llviewerregion.h"
#include "llviewerwindow.h"
#include "pipeline.h"

#include <iterator>

LLFloater360Capture::LLFloater360Capture(const LLSD& key)
    :   LLFloater(key)
{
    // The handle to embedded browser that we use to
    // render the WebGL preview as we as host the
    // Cube Map to Equirectangular image code
    mWebBrowser = nullptr;

    // Ask the simulator to send us everything (and not just
    // what it thinks the connected Viewer can see) until
    // such time as we ask it not to (the dtor). If we crash or
    // otherwise, exit before this is turned off, the Simulator
    // will take care of cleaning up for us.
    mStartILMode = gAgent.getInterestListMode();

    // send everything to us for as long as this floater is open
    gAgent.changeInterestListMode(LLViewerRegion::IL_MODE_360);
}

LLFloater360Capture::~LLFloater360Capture()
{
    if (mWebBrowser)
    {
        mWebBrowser->navigateStop();
        mWebBrowser->clearCache();
        mWebBrowser->unloadMediaSource();
    }

    // Restore interest list mode to the state when started
    // Normally LLFloater360Capture tells the Simulator send everything
    // and now reverts to the regular "keyhole" frustum of interest
    // list updates.
    if (!LLApp::isExiting() && 
        gSavedSettings.getBOOL("360CaptureUseInterestListCap") &&
        mStartILMode != gAgent.getInterestListMode())
    {
        gAgent.changeInterestListMode(mStartILMode);
    }
}

BOOL LLFloater360Capture::postBuild()
{
    mCaptureBtn = getChild<LLUICtrl>("capture_button");
    mCaptureBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onCapture360ImagesBtn, this));

    mSaveLocalBtn = getChild<LLUICtrl>("save_local_button");
    mSaveLocalBtn->setCommitCallback(boost::bind(&LLFloater360Capture::onSaveLocalBtn, this));
    mSaveLocalBtn->setEnabled(false);

    mWebBrowser = getChild<LLMediaCtrl>("360capture_contents");
    mWebBrowser->addObserver(this);
    mWebBrowser->setAllowFileDownload(true);

    // There is a group of radio buttons that define the quality
    // by each having a 'value' that is returns equal to the pixel
    // size (width == height)
    mQualityRadioGroup = getChild<LLRadioGroup>("360_quality_selection");
    mQualityRadioGroup->setCommitCallback(boost::bind(&LLFloater360Capture::onChooseQualityRadioGroup, this));

    // UX/UI called for preview mode (always the first index/option)
    // by default each time vs restoring the last value
    mQualityRadioGroup->setSelectedIndex(0);

    return true;
}

void LLFloater360Capture::onOpen(const LLSD& key)
{
    // Construct a URL pointing to the first page to load. Although
    // we do not use this page for anything (after some significant
    // design changes), we retain the code to load the start page
    // in case that changes again one day. It also makes sure the
    // embedded browser is active and ready to go for when the real
    // page with the 360 preview is navigated to.
    std::string url = STRINGIZE(
                          "file:///" <<
                          getHTMLBaseFolder() <<
                          mDefaultHTML
                      );
    mWebBrowser->navigateTo(url);

    // initial pass at determining what size (width == height since
    // the cube map images are square) we should capture at.
    setSourceImageSize();

    // the size of the output equirectangular image. The height of an EQR image
    // is always 1/2 of the width so we should not store it but rather,
    // calculate it from the width directly
    mOutputImageWidth = gSavedSettings.getU32("360CaptureOutputImageWidth");
    mOutputImageHeight = mOutputImageWidth / 2;

    // enable resizing and enable for width and for height
    enableResizeCtrls(true, true, true);

    // initial heading that consumers of the equirectangular image
    // (such as Facebook or Flickr) use to position initial view -
    // we set during capture - stored as degrees (0..359)
    mInitialHeadingDeg = 0.0;

    // save directory in which to store the images (must obliviously be
    // writable by the viewer). Also create it for users who haven't
    // used the 360 feature before.
    mImageSaveDir = gDirUtilp->getLindenUserDir() + gDirUtilp->getDirDelimiter() + "eqrimg";
    LLFile::mkdir(mImageSaveDir);

    // We do an initial capture when the floater is opened, albeit at a 'preview'
    // quality level (really low resolution, but really fast)
    onCapture360ImagesBtn();
}

// called when the user choose a quality level using
// the buttons in the radio group
void LLFloater360Capture::onChooseQualityRadioGroup()
{
    // set the size of the captured cube map images based
    // on the quality level chosen
    setSourceImageSize();
}


// There is is a setting (360CaptureSourceImageSize) that holds the size
// (width == height since it's a square) of each of the 6 source snapshots.
// However there are some known (and I dare say, some more unknown conditions
// where the specified size is not possible and this function tries to figure it
// out and change that setting to the optimal value for the current conditions.
void LLFloater360Capture::setSourceImageSize()
{
    mSourceImageSize = mQualityRadioGroup->getSelectedValue().asInteger();

    // If deferred rendering is off, we need to shrink the window we capture
    // until it's smaller than the Viewer window dimensions.
    if (!LLPipeline::sRenderDeferred)
    {
        LLRect window_rect = gViewerWindow->getWindowRectRaw();
        S32 window_width = window_rect.getWidth();
        S32 window_height = window_rect.getHeight();

        // It's not possible (as I had hoped) to always render to an off screen
        // buffer regardless of deferred rendering status so the next best
        // option is to render to a buffer that is the size of the users app
        // window.  Note, this was changed - before it chose the smallest
        // power of 2 less than the window size - but since that meant a
        // 1023 height app window would result in a 512 pixel capture, Maxim
        // tried this and it does indeed appear to work.  Mayb need to revisit
        // after the project viewer pass if people on low end graphics systems
        // after having issues.
        if (mSourceImageSize > window_width || mSourceImageSize > window_height)
        {
            mSourceImageSize = llmin(window_width, window_height, mSourceImageSize);
            LL_INFOS("360Capture") << "Deferred rendering is forcing a smaller capture size: " << mSourceImageSize << LL_ENDL;
        }

        // there has to be an easier way than this to get the value
        // from the radio group item at index 0. Why doesn't
        // LLRadioGroup::getSelectedValue(int index) exist?
        int index = mQualityRadioGroup->getSelectedIndex();
        mQualityRadioGroup->setSelectedIndex(0);
        int min_size = mQualityRadioGroup->getSelectedValue().asInteger();
        mQualityRadioGroup->setSelectedIndex(index);

        // If the maximum size we can support falls below a threshold then
        // we should display a message in the log so we can try to debug
        // why this is happening
        if (mSourceImageSize < min_size)
        {
            LL_INFOS("360Capture") << "Small snapshot size due to deferred rendering and small app window" << LL_ENDL;
        }
    }
}

// This function shouldn't exist! We use the tooltip text from
// the corresponding XUI file (floater_360capture.xml) as the
// overlay text for the final web page to inform the user
// about the quality level in play.  There ought to be a
// UI function like LLView* getSelectedItemView() or similar
// but as far as I can tell, there isn't so we have to resort
// to finding it ourselves with this icky code..
const std::string LLFloater360Capture::getSelectedQualityTooltip()
{
    // safey (or bravery?)
    if (mQualityRadioGroup != nullptr)
    {
        // for all the child widgets for the radio group
        // (just the radio buttons themselves I think)
        for (child_list_const_reverse_iter_t iter = mQualityRadioGroup->getChildList()->rbegin();
                iter != mQualityRadioGroup->getChildList()->rend();
                ++iter)
        {
            // if we match the selected index (which we can get easily)
            // with our position in the list of children
            if (mQualityRadioGroup->getSelectedIndex() ==
                    std::distance(mQualityRadioGroup->getChildList()->rend(), iter) - 1)
            {
                // return the plain old tooltip text
                return (*iter)->getToolTip();
            }
        }
    }

    // if it's not found or not available, return an empty string
    return std::string();
}

// Some of the 'magic' happens via a web page in an HTML directory
// and this code provides a single point of reference for its' location
const std::string LLFloater360Capture::getHTMLBaseFolder()
{
    std::string folder_name = gDirUtilp->getSkinDir();
    folder_name += gDirUtilp->getDirDelimiter();
    folder_name += "html";
    folder_name += gDirUtilp->getDirDelimiter();
    folder_name += "common";
    folder_name += gDirUtilp->getDirDelimiter();
    folder_name += "equirectangular";
    folder_name += gDirUtilp->getDirDelimiter();

    return folder_name;
}

// triggered when the 'capture' button in the UI is pressed
void LLFloater360Capture::onCapture360ImagesBtn()
{
    capture360Images();
}

// Gets the full path name for a given JavaScript file in the HTML folder. We
// use this ultimately as a parameter to the main web app so it knows where to find
// the JavaScript array containing the 6 cube map images, stored as data URLs
const std::string LLFloater360Capture::makeFullPathToJS(const std::string filename)
{
    std::string full_js_path = mImageSaveDir;
    full_js_path += gDirUtilp->getDirDelimiter();
    full_js_path += filename;

    return full_js_path;
}

// Write the header/prequel portion of the JavaScript array of data urls
// that we use to store the cube map images in (so the web code can load
// them without tweaking browser security - we'd have to do this if they
// we stored as plain old images) This deliberately overwrites the old
// one, if it exists
void LLFloater360Capture::writeDataURLHeader(const std::string filename)
{
    llofstream file_handle(filename.c_str());
    if (file_handle.is_open())
    {
        file_handle << "// cube map images for Second Life Viewer panorama 360 images" << std::endl;
        file_handle.close();
    }
}

// Write the footer/sequel portion of the JavaScript image code. When this is
// called, the current file on disk will contain the header and the 6 data
// URLs, each with a well specified name.  This final piece of JavaScript code
// creates an array from those data URLs that the main application can
// reference and read.
void LLFloater360Capture::writeDataURLFooter(const std::string filename)
{
    llofstream file_handle(filename.c_str(), std::ios_base::app);
    if (file_handle.is_open())
    {
        file_handle << "var cubemap_img_js = [" << std::endl;
        file_handle << "    img_posx, img_negx," << std::endl;
        file_handle << "    img_posy, img_negy," << std::endl;
        file_handle << "    img_posz, img_negz," << std::endl;
        file_handle << "];" << std::endl;

        file_handle.close();
    }
}

// Given a filename, a chunk of data (representing an image file) and the size
// of the buffer, we create a BASE64 encoded string and use it to build a JavaScript
// data URL that represents the image in a web browser environment
bool LLFloater360Capture::writeDataURL(const std::string filename, const std::string prefix, U8* data, unsigned int data_len)
{
    LL_INFOS("360Capture") << "Writing data URL for " << prefix << " to " << filename << LL_ENDL;

    const std::string data_url = LLBase64::encode(data, data_len);

    llofstream file_handle(filename.c_str(), std::ios_base::app);
    if (file_handle.is_open())
    {
        file_handle << "var img_";
        file_handle << prefix;
        file_handle << " = '";
        file_handle << "data:image/jpeg; base64,";
        file_handle << data_url;
        file_handle << "'";
        file_handle << std::endl;
        file_handle.close();

        return true;
    }

    return false;
}

// Encode the image from each of the 6 snapshots and save it out to
// the JavaScript array of data URLs
void LLFloater360Capture::encodeAndSave(LLPointer<LLImageRaw> raw_image, const std::string filename, const std::string prefix)
{
    // the default quality for the JPEG encoding is set quite high
    // but this still seems to be a reasonable compromise for
    // quality/size and is still much smaller than equivalent PNGs
    int jpeg_encode_quality = gSavedSettings.getU32("360CaptureJPEGEncodeQuality");
    LLPointer<LLImageJPEG> jpeg_image = new LLImageJPEG(jpeg_encode_quality);

    LLImageDataSharedLock lock(raw_image);

    // Actually encode the JPEG image. This is where a lot of time
    // is spent now that the snapshot capture process has been
    // optimized.  The encode_time parameter doesn't appear to be
    // used anymore.
    const int encode_time = 0;
    bool resultjpeg = jpeg_image->encode(raw_image, encode_time);

    if (resultjpeg)
    {
        // save individual cube map images as real JPEG files
        // for debugging or curiosity) based on debug settings
        if (gSavedSettings.getBOOL("360CaptureDebugSaveImage"))
        {
            const std::string jpeg_filename = STRINGIZE(
                                                  gDirUtilp->getLindenUserDir() <<
                                                  gDirUtilp->getDirDelimiter() <<
                                                  "eqrimg" <<
                                                  gDirUtilp->getDirDelimiter() <<
                                                  prefix <<
                                                  "." <<
                                                  jpeg_image->getExtension()
                                              );

            LL_INFOS("360Capture") << "Saving debug JPEG image as " << jpeg_filename << LL_ENDL;
            jpeg_image->save(jpeg_filename);
        }

        // actually write the JPEG image to disk as a data URL
        writeDataURL(filename, prefix, jpeg_image->getData(), jpeg_image->getDataSize());
    }
}

// Defer back to the main loop for a single rendered frame to give
// the renderer a chance to update the UI if it is needed
void LLFloater360Capture::suspendForAFrame()
{
    const U32 frame_count_delta = 1;
    U32 curr_frame_count = LLFrameTimer::getFrameCount();
    while (LLFrameTimer::getFrameCount() <= curr_frame_count + frame_count_delta)
    {
        llcoro::suspend();
    }
}

// A debug version of the snapshot code that simply fills the
// buffer with a pattern that can be used to investigate
// issues with encoding and saving off each RAW image.
// Probably not needed anymore but saving here just in case.
void LLFloater360Capture::mockSnapShot(LLImageRaw* raw)
{
    LLImageDataLock lock(raw);

    unsigned int width = raw->getWidth();
    unsigned int height = raw->getHeight();
    unsigned int depth = raw->getComponents();
    unsigned char* pixels = raw->getData();

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            unsigned long offset = y * width * depth + x * depth;
            unsigned char red = x * 256 / width;
            unsigned char green = y * 256 / height;
            unsigned char blue = ((x + y) / 2) * 256 / (width + height) / 2;
            pixels[offset + 0] = red;
            pixels[offset + 1] = green;
            pixels[offset + 2] = blue;
        }
    }
}

// The main code that actually captures all 6 images and then saves them out to
// disk before navigating the embedded web browser to the page with the WebGL
// application that consumes them and creates an EQR image. This code runs as a
// coroutine so it can be suspended at certain points.
void LLFloater360Capture::capture360Images()
{
    // recheck the size of the cube map source images in case it changed
    // since it was set when we opened the floater
    setSourceImageSize();

    // disable buttons while we are capturing
    mCaptureBtn->setEnabled(false);
    mSaveLocalBtn->setEnabled(false);

    bool render_attached_lights = LLPipeline::sRenderAttachedLights;
    // determine whether or not to include avatar in the scene as we capture the 360 panorama
    if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
    {
        // Turn off the avatar if UI tells us to hide it.
        // Note: the original call to gAvatar.hide(FALSE) did *not* hide
        // attachments and so for most residents, there would be some debris
        // left behind in the snapshot.
        // Note: this toggles so if it set to on, this will turn it off and
        // the subsequent call to the same thing after capture is finished
        // will turn it back on again.  Similarly, for the case where it
        // was set to off - I think this is what we need
        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES);
        LLPipeline::sRenderAttachedLights = FALSE;
    }

    // these are the 6 directions we will point the camera - essentially,
    // North, South, East, West, Up, Down
    LLVector3 look_dirs[6] = { LLVector3(1, 0, 0), LLVector3(0, 1, 0), LLVector3(0, 0, 1), LLVector3(-1, 0, 0), LLVector3(0, -1, 0), LLVector3(0, 0, -1) };
    LLVector3 look_upvecs[6] = { LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, -1, 0), LLVector3(0, 0, 1), LLVector3(0, 0, 1), LLVector3(0, 1, 0) };

    // save current view/camera settings so we can restore them afterwards
    S32 old_occlusion = LLPipeline::sUseOcclusion;

    // set new parameters specific to the 360 requirements
    LLPipeline::sUseOcclusion = 0;
    LLViewerCamera* camera = LLViewerCamera::getInstance();
    F32 old_fov = camera->getView();
    F32 old_aspect = camera->getAspect();
    F32 old_yaw = camera->getYaw();

    // stop the motion of as much of the world moving as much as we can
    freezeWorld(true);

    // Save the direction (in degrees) the camera is looking when we
    // take the shot since that is what we write to image metadata
    // 'GPano:InitialViewHeadingDegrees' field.
    // We need to convert from the angle getYaw() gives us into something
    // the XMP data field wants (N=0, E=90, S=180, W= 270 etc.)
    mInitialHeadingDeg  = (360 + 90 - (int)(camera->getYaw() * RAD_TO_DEG)) % 360;
    LL_INFOS("360Capture") << "Recording a heading of " << (int)(mInitialHeadingDeg)
        << " Image size: " << (S32)mSourceImageSize << LL_ENDL;

    // camera constants for the square, cube map capture image
    camera->setAspect(1.0); // must set aspect ratio first to avoid undesirable clamping of vertical FoV
    camera->setView(F_PI_BY_TWO);
    camera->yaw(0.0);

    // record how many times we changed camera to try to understand the "all shots are the same issue"
    unsigned int camera_changed_times = 0;

    // the name of the JavaScript file written out that contains the 6 cube map images
    // stored as a JavaScript array of data URLs.  If you change this filename, you must
    // also change the corresponding entry in the HTML file that uses it -
    // (newview/skins/default/html/common/equirectangular/display_eqr.html)
    const std::string cumemap_js_filename("cubemap_img.js");

    // construct the full path to this file - typically stored in the users'
    // Second Life settings / username / eqrimg folder.
    const std::string cubemap_js_full_path = makeFullPathToJS(cumemap_js_filename);

    // Write the JavaScript file header (the top of the file before the
    // declarations of the actual data URLs array).  In practice, all this writes
    // is a comment - it's main purpose is to reset the file from the last time
    // it was written
    writeDataURLHeader(cubemap_js_full_path);

    // the names of the prefixes we assign as the name to each data URL and are then
    // consumed by the WebGL application. Nominally, they stand for positive and
    // negative in the X/Y/Z directions.
    static const std::string prefixes[6] =
    {
        "posx", "posz", "posy",
        "negx", "negz", "negy",
    };

    // number of times to render the scene (display(..) inside
    // the simple snapshot function in llViewerWindow.
    // Note: rendering even just 1 more time (for a total of 2)
    // has a dramatic effect on the scene contents and *much*
    // less of it is missing. More investigation required
    // but for the moment, this helps with missing content
    // because of interest list issues.
    int num_render_passes = gSavedSettings.getU32("360CaptureNumRenderPasses");

    // time the encode process for later optimization
    auto encode_time_total = 0.0;

    // for each of the 6 directions we shoot...
    for (int i = 0; i < 6; i++)
    {
        LLAppViewer::instance()->pauseMainloopTimeout();
        LLViewerStats::instance().getRecording().stop();

        // these buffers are where the raw, captured pixels are stored and
        // the first time we use them, we have to make a new one
        if (mRawImages[i] == nullptr)
        {
            mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
        }
        else
            // subsequent capture with floater open so we resize the buffer from
            // the previous run
        {
            // LLImageRaw deletes the old one via operator= but just to be
            // sure, we delete its' large data member first...
            mRawImages[i]->deleteData();
            mRawImages[i] = new LLImageRaw(mSourceImageSize, mSourceImageSize, 3);
        }

        // set up camera to look in each direction
        camera->lookDir(look_dirs[i], look_upvecs[i]);

        // record if camera changed to try to understand the "all shots are the same issue"
        if (camera->isChanged())
        {
            ++camera_changed_times;
        }

        // call the (very) simplified snapshot code that simply deals
        // with a single image, no sub-images etc. but is very fast
        gViewerWindow->simpleSnapshot(mRawImages[i],
                                      mSourceImageSize, mSourceImageSize, num_render_passes);

        // encode each image and write to disk while saving how long it took to do so
        auto t_start = std::chrono::high_resolution_clock::now();
        encodeAndSave(mRawImages[i], cubemap_js_full_path, prefixes[i]);
        auto t_end = std::chrono::high_resolution_clock::now();
        auto duration = std::chrono::duration_cast<std::chrono::duration<double>>(t_end - t_start);
        encode_time_total += duration.count();

        LLViewerStats::instance().getRecording().resume();
        LLAppViewer::instance()->resumeMainloopTimeout();
        
        // update main loop timeout state
        LLAppViewer::instance()->pingMainloopTimeout("LLFloater360Capture::capture360Images");
    }

    // display time to encode all 6 images.  It tends to be a fairly linear
    // time for each so we don't need to worry about displaying the time
    // for each - this gives us plenty to use for optimizing
    LL_INFOS("360Capture") << "Time to encode and save 6 images was " <<
                           encode_time_total << " seconds" << LL_ENDL;

    // Write the JavaScript file footer (the bottom of the file after the
    // declarations of the actual data URLs array). The footer comprises of
    // a JavaScript array declaration that references the 6 data URLs generated
    // previously and is what is referred to in the display HTML file
    // (newview/skins/default/html/common/equirectangular/display_eqr.html)
    writeDataURLFooter(cubemap_js_full_path);

    // unfreeze the world now we have our shots
    freezeWorld(false);

    // restore original view/camera/avatar settings settings
    camera->setAspect(old_aspect);
    camera->setView(old_fov);
    camera->yaw(old_yaw);
    LLPipeline::sUseOcclusion = old_occlusion;

    // if we toggled off the avatar because the Hide check box was ticked,
    // we should toggle it back to where it was before we started the capture
    if (gSavedSettings.getBOOL("360CaptureHideAvatars"))
    {
        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_AVATAR);
        LLPipeline::toggleRenderTypeControl(LLPipeline::RENDER_TYPE_PARTICLES);
        LLPipeline::sRenderAttachedLights = render_attached_lights;
    }

    // record that we missed some shots in the log for later debugging
    // note: we use 5 and not 6 because the first shot isn't regarded
    // as a change - only the subsequent 5 are
    if (camera_changed_times < 5)
    {
        LL_WARNS("360Capture") << "360 image capture expected 5 or more images, only captured " << camera_changed_times << " images." << LL_ENDL;
    }

    // now we have the 6 shots saved in a well specified location,
    // we can load the web content that uses them
    std::string url = "file:///" + getHTMLBaseFolder() + mEqrGenHTML;
    mWebBrowser->navigateTo(url);

    // page is loaded and ready so we can turn on the buttons again
    mCaptureBtn->setEnabled(true);
    mSaveLocalBtn->setEnabled(true);

}

// once the request is made to navigate to the web page containing the code
// to process the 6 images into an EQR one, we have to wait for it to finish
// loaded - we get a "navigate complete" event when that happens that we can act on
void LLFloater360Capture::handleMediaEvent(LLPluginClassMedia* self, EMediaEvent event)
{
    switch (event)
    {
        // not used right now but retaining because this event might
        // be useful for a feature I am hoping to add
        case MEDIA_EVENT_LOCATION_CHANGED:
            break;

        // navigation in the browser completed
        case MEDIA_EVENT_NAVIGATE_COMPLETE:
        {
            // Confirm that the navigation event does indeed apply to the
            // page we are looking for. At the moment, this is the only
            // one we care about so the test is superfluous but that might change.
            std::string navigate_url = self->getNavigateURI();
            if (navigate_url.find(mEqrGenHTML) != std::string::npos)
            {
                // this string is being passed across to the web so replace all the windows backslash
                // characters with forward slashes or (I think) the backslashes are treated as escapes
                std::replace(mImageSaveDir.begin(), mImageSaveDir.end(), '\\', '/');

                // we store the camera FOV (field of view) in a saved setting since this feels
                // like something it would be interesting to change and experiment with
                int camera_fov = gSavedSettings.getU32("360CaptureCameraFOV");

                // compose the overlay for the final web page that tells the user
                // what level of quality the capture was taken with
                std::string overlay_label = "'" + getSelectedQualityTooltip() + "'";

                // so now our page is loaded and images are in place - call
                // the JavaScript init script with some parameters to initialize
                // the WebGL based preview
                const std::string cmd = STRINGIZE(
                                            "init("
                                            << mOutputImageWidth
                                            << ", "
                                            << mOutputImageHeight
                                            << ", "
                                            << "'"
                                            << mImageSaveDir
                                            << "'"
                                            << ", "
                                            << camera_fov
                                            << ", "
                                            << LLViewerCamera::getInstance()->getYaw()
                                            << ", "
                                            << overlay_label
                                            << ")"
                                        );

                // execute the command on the page
                mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
            }
        }
        break;

        default:
            break;
    }
}

// called when the user wants to save the cube maps off to the final EQR image
void LLFloater360Capture::onSaveLocalBtn()
{
    // region name and URL
    std::string region_name; // no sensible default
    std::string region_url("http://secondlife.com");
    LLViewerRegion* region = gAgent.getRegion();
    if (region)
    {
        // region names can (and do) contain characters that would make passing
        // them into a JavaScript function problematic - single quotes for example
        // so we must escape/encode both
        region_name = region->getName();

        // escaping/encoding is a minefield - let's just remove any offending characters from the region name
        region_name.erase(std::remove(region_name.begin(), region_name.end(), '\''), region_name.end());
        region_name.erase(std::remove(region_name.begin(), region_name.end(), '\"'), region_name.end());

        // fortunately there is already an escaping function built into the SLURL generation code
        LLSLURL slurl;
        bool is_escaped = true;
        LLAgentUI::buildSLURL(slurl, is_escaped);
        region_url = slurl.getSLURLString();
    }

    // build suggested filename (the one that appears as the default
    // in the Save dialog box)
    const std::string suggested_filename = generate_proposed_filename();

    // This string (the name of the product plus a truncated version number (no build))
    // is used in the XMP block as the name of the generating and stitching software.
    // We save the version number here and not in the more generic 'software' item
    // because that might help us determine something about the image in the future.
    const std::string client_version = STRINGIZE(
                                           LLVersionInfo::instance().getChannel() <<
                                           " " <<
                                           LLVersionInfo::instance().getShortVersion()
                                       );

    // save the time the image was created. I don't know if this should be
    // UTC/ZULU or the users' local time. It probably doesn't matter.
    std::time_t result = std::time(nullptr);
    std::string ctime_str = std::ctime(&result);
    std::string time_str = ctime_str.substr(0, ctime_str.length() - 1);

    // build the JavaScript data structure that is used to pass all the
    // variables into the JavaScript function on the web page loaded into
    // the embedded browser component of the floater.
    const std::string xmp_details = STRINGIZE(
                                        "{ " <<
                                        "pano_version: '" << "2.2.1" << "', " <<
                                        "software: '" << LLVersionInfo::instance().getChannel() << "', " <<
                                        "capture_software: '" << client_version << "', " <<
                                        "stitching_software: '" << client_version << "', " <<
                                        "width: " << mOutputImageWidth << ", " <<
                                        "height: " << mOutputImageHeight << ", " <<
                                        "heading: " << mInitialHeadingDeg << ", " <<
                                        "actual_source_image_size: " << mQualityRadioGroup->getSelectedValue().asInteger() << ", " <<
                                        "scaled_source_image_size: " << mSourceImageSize << ", " <<
                                        "first_photo_date: '" << time_str << "', " <<
                                        "last_photo_date: '" << time_str << "', " <<
                                        "region_name: '" << region_name << "', " <<
                                        "region_url: '" << region_url << "', " <<
                                        " }"
                                    );

    // build the JavaScript command to send to the web browser
    const std::string cmd = "saveAsEqrImage(\"" + suggested_filename + "\", " + xmp_details + ")";

    // send it to the browser instance, triggering the equirectangular capture
    // process and complimentary offer to save the image
    mWebBrowser->getMediaPlugin()->executeJavaScript(cmd);
}

// We capture all 6 images sequentially and if parts of the world are moving
// E.G. clouds, water, objects - then we may get seams or discontinuities
// when the images are combined to form the EQR image.  This code tries to
// stop everything so we can shoot for seamless shots.  There is probably more
// we can do here - e.g. waves in the water probably won't line up.
void LLFloater360Capture::freezeWorld(bool enable)
{
    static bool clouds_scroll_paused = false;
    if (enable)
    {
        // record the cloud scroll current value so we can restore it
        clouds_scroll_paused = LLEnvironment::instance().isCloudScrollPaused();

        // stop the clouds moving
        LLEnvironment::instance().pauseCloudScroll();

        // freeze all avatars
        LLCharacter* avatarp;
        for (std::vector<LLCharacter*>::iterator iter = LLCharacter::sInstances.begin();
                iter != LLCharacter::sInstances.end(); ++iter)
        {
            avatarp = *iter;
            mAvatarPauseHandles.push_back(avatarp->requestPause());
        }

        // freeze everything else
        gSavedSettings.setBOOL("FreezeTime", true);

        // disable particle system
        LLViewerPartSim::getInstance()->enable(false);
    }
    else // turning off freeze world mode, either temporarily or not.
    {
        // restart the clouds moving if they were not paused before
        // we starting using the 360 capture floater
        if (clouds_scroll_paused == false)
        {
            LLEnvironment::instance().resumeCloudScroll();
        }

        // thaw all avatars
        mAvatarPauseHandles.clear();

        // thaw everything else
        gSavedSettings.setBOOL("FreezeTime", false);

        //enable particle system
        LLViewerPartSim::getInstance()->enable(true);
    }
}

// Build the default filename that appears in the Save dialog box. We try
// to encode some metadata about too (region name, EQR dimensions, capture
// time) but the user if free to replace this with anything else before
// the images is saved.
const std::string LLFloater360Capture::generate_proposed_filename()
{
    std::ostringstream filename("");

    // base name
    filename << "sl360_";

    LLViewerRegion* region = gAgent.getRegion();
    if (region)
    {
        // this looks complex but it's straightforward - removes all non-alpha chars from a string
        // which in this case is the SL region name - we use it as a proposed filename but the user is free to change
        std::string region_name = region->getName();
        std::replace_if(region_name.begin(), region_name.end(),
                        [](char c){ return ! std::isalnum(c); },
                        '_');
        if (! region_name.empty())
        {
            filename << region_name;
            filename << "_";
        }
    }

    // add in resolution to make it easier to tell what you captured later
    filename << mOutputImageWidth;
    filename << "x";
    filename << mOutputImageHeight;
    filename << "_";

    // Add in the size of the source image (width == height since it was square)
    // Might be useful later for quality comparisons
    filename << mSourceImageSize;
    filename << "_";

    // add in the current HH-MM-SS (with leading 0's) so users can easily save many shots in same folder
    std::time_t cur_epoch = std::time(nullptr);
    std::tm* tm_time = std::localtime(&cur_epoch);
    filename << std::setfill('0') << std::setw(4) << (tm_time->tm_year + 1900);
    filename << std::setfill('0') << std::setw(2) << (tm_time->tm_mon + 1);
    filename << std::setfill('0') << std::setw(2) << tm_time->tm_mday;
    filename << "_";
    filename << std::setfill('0') << std::setw(2) << tm_time->tm_hour;
    filename << std::setfill('0') << std::setw(2) << tm_time->tm_min;
    filename << std::setfill('0') << std::setw(2) << tm_time->tm_sec;

    // the unusual way we save the output image (originates in the
    // embedded browser and not the C++ code) means that the system
    // appends ".jpeg" to the file automatically on macOS at least,
    // so we only need to do it ourselves for windows.
#if LL_WINDOWS
    filename << ".jpg";
#endif

    return filename.str();
}