1 /*
2 * Copyright (C) 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012
3 * Free Software Foundation, Inc.
4 *
5 * This program is free software; you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation; either version 3 of the License, or
8 * (at your option) any later version.
9 *
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 *
15 * You should have received a copy of the GNU General Public License
16 * along with this program; if not, write to the Free Software
17 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18 *
19 *
20 */
21
22 #include "MovieTester.h"
23 #include "GnashException.h"
24 #include "URL.h"
25 #include "noseek_fd_adapter.h"
26 #include "movie_definition.h"
27 #include "Movie.h"
28 #include "movie_root.h"
29 #include "MovieClip.h"
30 #include "MovieFactory.h"
31 #include "sound_handler.h" // for creating the "test" sound handlers
32 #include "NullSoundHandler.h"
33 #include "RGBA.h" // for rgba class (pixel checking)
34 #include "FuzzyPixel.h" // for pixel checking
35 #include "Renderer.h"
36 #include "ManualClock.h" // for use by advance
37 #include "StreamProvider.h" // for passing to RunResources
38 #include "IOChannel.h"
39 #include "swf/TagLoadersTable.h"
40 #include "swf/DefaultTagLoaders.h"
41 #include "GnashFactory.h"
42
43 #ifdef RENDERER_CAIRO
44 # include "Renderer_cairo.h"
45 #endif
46 #ifdef RENDERER_OPENGL
47 # include "Renderer_ogl.h"
48 #endif
49 #ifdef RENDERER_AGG
50 # include "Renderer_agg.h"
51 #endif
52
53 #include "MediaHandler.h"
54
55 #include <cstdio>
56 #include <string>
57 #include <memory> // for unique_ptr
58 #include <cmath> // for ceil
59 #include <iostream>
60
61 //#define SHOW_INVALIDATED_BOUNDS_ON_ADVANCE 1
62
63 #ifdef SHOW_INVALIDATED_BOUNDS_ON_ADVANCE
64 #include <sstream>
65 #endif
66
67
68 using std::cout;
69 using std::endl;
70
71 namespace gnash {
72
73 namespace {
74 // exp2 isn't part of standard C++, so is defined here in case the compiler
75 // doesn't supply it (e.g. in BSD)
exp2(double x)76 inline double exp2(double x) { return std::pow(2.0, x); }
77
78 bool getAveragePixel(const Renderer& r, rgba& color_return, int x, int y,
79 unsigned int radius);
80
toShortString(const rgba & r)81 std::string toShortString(const rgba& r) {
82 std::stringstream ss;
83 ss << +r.m_r << "," << +r.m_g << "," << +r.m_b << "," << +r.m_a;
84 return ss.str();
85 }
86 }
87
MovieTester(const std::string & url)88 MovieTester::MovieTester(const std::string& url)
89 :
90 _x(0),
91 _y(0),
92 _forceRedraw(true),
93 _samplesFetched(0)
94 {
95
96 #ifdef USE_MEDIA
97 // Initialize the testing media handlers
98 initTestingMediaHandlers();
99 #endif
100
101 #ifdef USE_SOUND
102 // Initialize the sound handler(s)
103 initTestingSoundHandlers();
104 _runResources.setSoundHandler(_sound_handler);
105 #endif
106 #ifdef USE_MEDIA
107 _runResources.setMediaHandler(_mediaHandler);
108 #endif
109
110 std::shared_ptr<SWF::TagLoadersTable> loaders(new SWF::TagLoadersTable());
111 addDefaultLoaders(*loaders);
112
113 _runResources.setTagLoaders(loaders);
114
115 std::shared_ptr<StreamProvider> sp(new StreamProvider(url, url));
116
117 _runResources.setStreamProvider(sp);
118
119 if ( url == "-" ) {
120 std::unique_ptr<IOChannel> in (
121 noseek_fd_adapter::make_stream(fileno(stdin))
122 );
123 _movie_def = MovieFactory::makeMovie(std::move(in), url, _runResources, false);
124 } else {
125 URL urlObj(url);
126 if ( urlObj.protocol() == "file" ) {
127 RcInitFile& rcfile = RcInitFile::getDefaultInstance();
128 const std::string& path = urlObj.path();
129 #if 1 // add the *directory* the movie was loaded from to the local sandbox path
130 size_t lastSlash = path.find_last_of('/');
131 std::string dir = path.substr(0, lastSlash+1);
132 rcfile.addLocalSandboxPath(dir);
133 log_debug(_("%s appended to local sandboxes"), dir.c_str());
134 #else // add the *file* to be loaded to the local sandbox path
135 rcfile.addLocalSandboxPath(path);
136 log_debug(_("%s appended to local sandboxes"), path.c_str());
137 #endif
138 }
139 // _url should be always set at this point...
140 _movie_def = MovieFactory::makeMovie(urlObj, _runResources,
141 NULL, false);
142 }
143
144 if ( ! _movie_def ) {
145 throw GnashException("Could not load movie from "+url);
146 }
147
148 _movie_root.reset(new movie_root(_clock, _runResources));
149
150 // Initialize viewport size with the one advertised in the header
151 _width = unsigned(_movie_def->get_width_pixels());
152 _height = unsigned(_movie_def->get_height_pixels());
153
154 // Initialize the testing renderers
155 initTestingRenderers();
156
157 // Now complete load of the movie
158 _movie_def->completeLoad();
159 _movie_def->ensure_frame_loaded(_movie_def->get_frame_count());
160
161 // Activate verbosity so that self-contained testcases are
162 // also used
163 gnash::LogFile& dbglogfile = gnash::LogFile::getDefaultInstance();
164 dbglogfile.setVerbosity(1);
165
166 // Finally, place the root movie on the stage ...
167 MovieClip::MovieVariables v;
168 _movie_root->init(_movie_def.get(), v);
169
170 // ... and render it
171 render();
172 }
173
~MovieTester()174 MovieTester::~MovieTester()
175 {
176 MovieFactory::clear();
177 }
178
179 void
render(std::shared_ptr<Renderer> h,InvalidatedRanges & invalidated_regions)180 MovieTester::render(std::shared_ptr<Renderer> h,
181 InvalidatedRanges& invalidated_regions)
182 {
183
184 // This is a bit dangerous, as there isn't really support for swapping
185 // renderers during runtime; though the only problem is likely to be
186 // that CachedBitmaps are missing.
187 _runResources.setRenderer(h);
188
189 h->set_invalidated_regions(invalidated_regions);
190
191 // We call display here to simulate effect of a real run.
192 //
193 // What we're particularly interested about is
194 // proper computation of invalidated bounds, which
195 // needs clear_invalidated() to be called.
196 // display() will call clear_invalidated() on DisplayObjects
197 // actually modified so we're fine with that.
198 //
199 // Directly calling _movie->clear_invalidated() here
200 // also work currently, as invalidating the topmost
201 // movie will force recomputation of all invalidated
202 // bounds. Still, possible future changes might
203 // introduce differences, so better to reproduce
204 // real runs as close as possible, by calling display().
205 //
206 _movie_root->display();
207 }
208
209 void
redraw()210 MovieTester::redraw()
211 {
212 _forceRedraw=true;
213 render();
214 }
215
216 void
render()217 MovieTester::render()
218 {
219 // Get invalidated ranges and cache them
220 _invalidatedBounds.setNull();
221
222 _movie_root->add_invalidated_bounds(_invalidatedBounds, false);
223
224 #ifdef SHOW_INVALIDATED_BOUNDS_ON_ADVANCE
225 const MovieClip* r = getRootMovie();
226 std::cout << "frame " << r->get_current_frame() << ") Invalidated bounds " << _invalidatedBounds << std::endl;
227 #endif
228
229 // Force full redraw by using a WORLD invalidated ranges
230 InvalidatedRanges ranges = _invalidatedBounds;
231 if ( _forceRedraw ) {
232 ranges.setWorld(); // set to world if asked a full redraw
233 _forceRedraw = false; // reset to no forced redraw
234 }
235
236 for (TestingRenderers::const_iterator it=_testingRenderers.begin(),
237 itE=_testingRenderers.end(); it != itE; ++it) {
238 const TestingRenderer& rend = *it;
239 render(rend.getRenderer(), ranges);
240 }
241
242 if ( _testingRenderers.empty() ) {
243 // Make sure display is called in any case
244 //
245 // What we're particularly interested about is
246 // proper computation of invalidated bounds, which
247 // needs clear_invalidated() to be called.
248 // display() will call clear_invalidated() on DisplayObjects
249 // actually modified so we're fine with that.
250 //
251 // Directly calling _movie->clear_invalidated() here
252 // also work currently, as invalidating the topmost
253 // movie will force recomputation of all invalidated
254 // bounds. Still, possible future changes might
255 // introduce differences, so better to reproduce
256 // real runs as close as possible, by calling display().
257 //
258 _movie_root->display();
259 }
260 }
261
262 void
advanceClock(unsigned long ms_current)263 MovieTester::advanceClock(unsigned long ms_current)
264 {
265 _clock.advance(ms_current);
266
267 if ( _sound_handler ) {
268
269 unsigned int ms = _clock.elapsed();
270
271 // We need to fetch as many samples
272 // as needed for a theoretical 44100hz loop.
273 // That is 44100 samples each second.
274 // 44100/1000 = x/ms
275 // x = (44100*ms) / 1000
276 unsigned int nSamples = (441*ms) / 10;
277
278 // We double because sound_handler interface takes
279 // "mono" samples... (eh.. would be wise to change)
280 unsigned int toFetch = nSamples*2;
281
282 // Now substract what we fetched already
283 toFetch -= _samplesFetched;
284
285 // And update _samplesFetched..
286 _samplesFetched += toFetch;
287
288 log_debug("advanceClock(%d) needs to fetch %d samples", ms, toFetch);
289
290 std::int16_t samples[1024];
291 while (toFetch) {
292 unsigned int n = std::min(toFetch, 1024u);
293 _sound_handler->fetchSamples((std::int16_t*)&samples, n);
294 toFetch -= n;
295 }
296 }
297 }
298
299 void
advance(bool updateClock)300 MovieTester::advance(bool updateClock)
301 {
302 if ( updateClock ) {
303 // TODO: cache 'clockAdvance'
304 float fps = _movie_def->get_frame_rate();
305 unsigned long clockAdvance = long(1000/fps);
306 advanceClock(clockAdvance);
307 }
308
309 if (_movie_root->advance()) render();
310
311 }
312
313 void
resizeStage(int x,int y)314 MovieTester::resizeStage(int x, int y)
315 {
316 _movie_root->setDimensions(x, y);
317
318 if (_movie_root->getStageScaleMode() != movie_root::SCALEMODE_NOSCALE) {
319 // TODO: fix to deal with all scale modes
320 // and alignments ?
321
322 // set new scale value
323 float xscale = x / _movie_def->get_width_pixels();
324 float yscale = y / _movie_def->get_height_pixels();
325
326 if (xscale < yscale) yscale = xscale;
327 if (yscale < xscale) xscale = yscale;
328
329 // Scale for all renderers.
330 for (TestingRenderers::iterator it=_testingRenderers.begin(),
331 itE=_testingRenderers.end(); it != itE; ++it) {
332 TestingRenderer& rend = *it;
333 Renderer* h = rend.getRenderer().get();
334 h->set_scale(xscale, yscale);
335 }
336 }
337 }
338
339 const DisplayObject*
findDisplayItemByName(const MovieClip & mc,const std::string & name)340 MovieTester::findDisplayItemByName(const MovieClip& mc,
341 const std::string& name)
342 {
343 const DisplayList& dlist = mc.getDisplayList();
344 string_table& st = getStringTable(*getObject(&mc));
345 VM& vm = getVM(*getObject(&mc));
346 return dlist.getDisplayObjectByName(st, getURI(vm, name), false);
347 }
348
349 const DisplayObject*
findDisplayItemByTarget(const std::string & tgt)350 MovieTester::findDisplayItemByTarget(const std::string& tgt)
351 {
352 return _movie_root->findCharacterByTarget(tgt);
353 }
354
355 const DisplayObject*
findDisplayItemByDepth(const MovieClip & mc,int depth)356 MovieTester::findDisplayItemByDepth(const MovieClip& mc,
357 int depth)
358 {
359 const DisplayList& dlist = mc.getDisplayList();
360 return dlist.getDisplayObjectAtDepth(depth);
361 }
362
363 void
movePointerTo(int x,int y)364 MovieTester::movePointerTo(int x, int y)
365 {
366 _x = x;
367 _y = y;
368 if ( _movie_root->mouseMoved(x, y) ) render();
369 }
370
371 void
checkPixel(int x,int y,unsigned radius,const rgba & color,short unsigned tolerance,const std::string & label,bool expectFailure) const372 MovieTester::checkPixel(int x, int y, unsigned radius, const rgba& color,
373 short unsigned tolerance, const std::string& label, bool expectFailure) const
374 {
375 if ( ! canTestRendering() ) {
376 std::stringstream ss;
377 ss << "exp:" << toShortString(color) << " ";
378 cout << "UNTESTED: NORENDERER: pix:" << x << "," << y << " exp:" <<
379 toShortString(color) << " " << label << endl;
380 }
381
382 FuzzyPixel exp(color, tolerance);
383 const char* X="";
384 if ( expectFailure ) X="X";
385
386 //std::cout <<"chekPixel(" << color << ") called" << std::endl;
387
388 for (TestingRenderers::const_iterator it=_testingRenderers.begin(),
389 itE=_testingRenderers.end(); it != itE; ++it) {
390 const TestingRenderer& rend = *it;
391
392 std::stringstream ss;
393 ss << rend.getName() <<" ";
394 ss << "pix:" << x << "," << y <<" ";
395
396 rgba obt_col;
397
398 const Renderer& handler = *rend.getRenderer();
399
400 if (!getAveragePixel(handler, obt_col, x, y, radius) ) {
401 ss << " is out of rendering buffer";
402 cout << X << "FAILED: " << ss.str() << " (" << label << ")" << endl;
403 continue;
404 }
405
406 // Find minimum tolerance as a function of BPP
407
408 unsigned short minRendererTolerance = 1;
409 unsigned int bpp = handler.getBitsPerPixel();
410 if ( bpp ) {
411 // UdoG: check_pixel should *always* tolerate at least 2 ^ (8 - bpp/3)
412 minRendererTolerance = int(std::ceil(exp2(8 - bpp/3)));
413 }
414
415 //unsigned short tol = std::max(tolerance, minRendererTolerance);
416 unsigned short tol = tolerance*minRendererTolerance;
417
418 ss << "exp:" << toShortString(color) << " ";
419 ss << "obt:" << toShortString(obt_col) << " ";
420 ss << "tol:" << tol;
421
422 FuzzyPixel obt(obt_col, tol);
423 // equality operator would use tolerance of most tolerating FuzzyPixel
424 if (exp == obt) {
425 cout << X << "PASSED: " << ss.str() << " (" << label << ")" << endl;
426 } else {
427 cout << X << "FAILED: " << ss.str() << " (" << label << ")" << endl;
428 }
429 }
430 }
431
432 void
pressMouseButton()433 MovieTester::pressMouseButton()
434 {
435 if ( _movie_root->mouseClick(true) ) {
436 render();
437 }
438 }
439
440 void
depressMouseButton()441 MovieTester::depressMouseButton()
442 {
443 if ( _movie_root->mouseClick(false) ) {
444 render();
445 }
446 }
447
448 void
click()449 MovieTester::click()
450 {
451 int wantRedraw = 0;
452 if ( _movie_root->mouseClick(true) ) ++wantRedraw;
453 if ( _movie_root->mouseClick(false) ) ++wantRedraw;
454
455 if ( wantRedraw ) render();
456 }
457
458 void
scrollMouse(int delta)459 MovieTester::scrollMouse(int delta)
460 {
461 if (_movie_root->mouseWheel(delta)) render();
462 }
463
464 void
pressKey(key::code code)465 MovieTester::pressKey(key::code code)
466 {
467 if ( _movie_root->keyEvent(code, true) ) {
468 render();
469 }
470 }
471
472 void
releaseKey(key::code code)473 MovieTester::releaseKey(key::code code)
474 {
475 if ( _movie_root->keyEvent(code, false) ) {
476 render();
477 }
478 }
479
480 bool
isMouseOverMouseEntity()481 MovieTester::isMouseOverMouseEntity()
482 {
483 return (_movie_root->getActiveEntityUnderPointer());
484 }
485
486 bool
usingHandCursor()487 MovieTester::usingHandCursor()
488 {
489 DisplayObject* activeEntity = _movie_root->getActiveEntityUnderPointer();
490 if ( ! activeEntity ) return false;
491
492 if ( activeEntity->isSelectableTextField() ) {
493 return false; // setCursor(CURSOR_INPUT);
494 } else if ( activeEntity->allowHandCursor() ) {
495 return true; // setCursor(CURSOR_HAND);
496 } else {
497 return false; // setCursor(CURSOR_NORMAL);
498 }
499 }
500
501 geometry::SnappingRanges2d<int>
getInvalidatedRanges() const502 MovieTester::getInvalidatedRanges() const
503 {
504 using namespace gnash::geometry;
505
506 SnappingRanges2d<float> ranges = _invalidatedBounds;
507
508 // scale by 1/20 (twips to pixels)
509 ranges.scale(1.0/20);
510
511 // Convert to integer range.
512 SnappingRanges2d<int> pixranges(ranges);
513
514 return pixranges;
515
516 }
517
518 bool
streamingSound() const519 MovieTester::streamingSound() const
520 {
521 if (!_sound_handler.get()) return false;
522 #ifdef USE_SOUND
523 return _sound_handler->streamingSound();
524 #endif
525 }
526
527 int
soundsStarted()528 MovieTester::soundsStarted()
529 {
530 if ( ! _sound_handler.get() ) return 0;
531 #ifdef USE_SOUND
532 return _sound_handler->numSoundsStarted();
533 #endif
534 }
535
536 int
soundsStopped()537 MovieTester::soundsStopped()
538 {
539 if ( ! _sound_handler.get() ) return 0;
540 return _sound_handler->numSoundsStopped();
541 }
542
543 void
initTestingRenderers()544 MovieTester::initTestingRenderers()
545 {
546 std::shared_ptr<Renderer> handler;
547
548 // TODO: add support for testing multiple renderers
549 // This is tricky as requires changes in the core lib
550
551 #ifdef RENDERER_AGG
552 // Initialize AGG
553 static const char* aggPixelFormats[] = {
554 "RGB555", "RGB565", "RGBA16",
555 "RGB24", "BGR24", "RGBA32", "BGRA32",
556 "ARGB32", "ABGR32"
557 };
558
559 for (unsigned i=0; i<sizeof(aggPixelFormats)/sizeof(*aggPixelFormats); ++i) {
560 const char* pixelFormat = aggPixelFormats[i];
561 std::string name = "AGG_" + std::string(pixelFormat);
562
563 handler.reset( create_Renderer_agg(pixelFormat) );
564 if ( handler.get() ) {
565 //log_debug("Renderer %s initialized", name.c_str());
566 std::cout << "Renderer " << name << " initialized" << std::endl;
567 addTestingRenderer(handler, name);
568 } else {
569 std::cout << "Renderer " << name << " not supported" << std::endl;
570 }
571 }
572 #endif // RENDERER_AGG
573
574 #ifdef RENDERER_CAIRO
575 // Initialize Cairo
576 handler.reset(renderer::cairo::create_handler());
577
578 addTestingRenderer(handler, "Cairo");
579 #endif
580
581 #ifdef RENDERER_OPENGL
582 // Initialize opengl renderer
583 handler.reset(renderer::opengl::create_handler(false));
584 addTestingRenderer(handler, "OpenGL");
585 #endif
586 }
587
588 void
addTestingRenderer(std::shared_ptr<Renderer> h,const std::string & name)589 MovieTester::addTestingRenderer(std::shared_ptr<Renderer> h,
590 const std::string& name)
591 {
592 if ( ! h->initTestBuffer(_width, _height) ) {
593 std::cout << "UNTESTED: render handler " << name
594 << " doesn't support in-memory rendering "
595 << std::endl;
596 return;
597 }
598
599 // TODO: make the core lib support this
600 if ( ! _testingRenderers.empty() ) {
601 std::cout << "UNTESTED: can't test render handler " << name
602 << " because gnash core lib is unable to support testing of "
603 << "multiple renderers from a single process "
604 << "and we're already testing render handler "
605 << _testingRenderers.front().getName()
606 << std::endl;
607 return;
608 }
609
610 _testingRenderers.push_back(TestingRenderer(h, name));
611
612 // this will be needed till we allow run-time swapping of renderers,
613 // see above UNTESTED message...
614 _runResources.setRenderer(_testingRenderers.back().getRenderer());
615 }
616
617 bool
canTestVideo() const618 MovieTester::canTestVideo() const
619 {
620 if ( ! canTestSound() ) return false;
621
622 return true;
623 }
624
625 void
initTestingSoundHandlers()626 MovieTester::initTestingSoundHandlers()
627 {
628 #ifdef USE_SOUND
629 // Currently, SoundHandler can't be constructed
630 // w/out a registered MediaHandler .
631 // Should be fixed though...
632 if (_mediaHandler.get()) {
633 _sound_handler.reset(new sound::NullSoundHandler(_mediaHandler.get()));
634 } else {
635 log_error("No media handler available, "
636 "could not construct sound handler");
637 }
638 #endif // USE_SOUND
639 }
640
641 void
initTestingMediaHandlers()642 MovieTester::initTestingMediaHandlers()
643 {
644 #ifdef USE_MEDIA
645 // TODO: allow selection.
646 _mediaHandler.reset(media::MediaFactory::instance().get(""));
647 #endif
648 }
649
650 void
restart()651 MovieTester::restart()
652 {
653 _movie_root->reset();
654 MovieClip::MovieVariables v;
655 _movie_root->init(_movie_def.get(), v);
656
657 // Set _movie before calling ::render
658 render();
659 }
660
661 float
getFrameRate() const662 MovieTester::getFrameRate() const
663 {
664 return _movie_def->get_frame_rate();
665 }
666
667 namespace {
668
669 /// Returns the average RGB color for a square block on the stage. The
670 /// width and height of the block is defined by "radius" and x/y refer
671 /// to the center of the block. radius==1 equals getPixel() and radius==0
672 /// is illegal. For even "radius" values, the center point is not exactly
673 /// defined.
674 /// The function returns false when at least one pixel of the block was
675 /// outside the main frame buffer. In that case the value in color_return
676 /// is undefined.
getAveragePixel(const Renderer & rh,rgba & color_return,int x,int y,unsigned int radius)677 bool getAveragePixel(const Renderer& rh, rgba& color_return, int x, int y,
678 unsigned int radius)
679 {
680 assert(radius>0);
681
682 // optimization:
683 if (radius==1) return rh.getPixel(color_return, x, y);
684
685 unsigned int r=0, g=0, b=0, a=0;
686
687 x -= radius/2;
688 y -= radius/2;
689
690 int xe = x+radius;
691 int ye = y+radius;
692
693 rgba pixel;
694
695 for (int yp=y; yp<ye; yp++)
696 for (int xp=x; xp<xe; xp++) {
697 if (!rh.getPixel(pixel, xp, yp))
698 return false;
699
700 r += pixel.m_r;
701 g += pixel.m_g;
702 b += pixel.m_b;
703 a += pixel.m_a;
704 }
705
706 int pcount = radius*radius;
707 color_return.m_r = r / pcount;
708 color_return.m_g = g / pcount;
709 color_return.m_b = b / pcount;
710 color_return.m_a = a / pcount;
711
712 return true;
713 }
714
715 }
716
717 } // namespace gnash
718