| // Copyright 2015 The Cobalt Authors. All Rights Reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| #include <memory> |
| |
| #include "base/threading/simple_thread.h" |
| |
| #include "base/message_loop/message_loop.h" |
| #include "base/run_loop.h" |
| #include "cobalt/render_tree/resource_provider.h" |
| #include "cobalt/renderer/backend/default_graphics_system.h" |
| #include "cobalt/renderer/backend/graphics_context.h" |
| #include "cobalt/renderer/backend/graphics_system.h" |
| #include "cobalt/renderer/backend/render_target.h" |
| #include "cobalt/renderer/rasterizer/rasterizer.h" |
| #include "cobalt/renderer/renderer_module.h" |
| |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using cobalt::render_tree::ResourceProvider; |
| |
| namespace cobalt { |
| namespace renderer { |
| |
| namespace { |
| render_tree::PixelFormat ChoosePixelFormat( |
| ResourceProvider* resource_provider) { |
| if (resource_provider->PixelFormatSupported(render_tree::kPixelFormatRGBA8)) { |
| return render_tree::kPixelFormatRGBA8; |
| } else if (resource_provider->PixelFormatSupported( |
| render_tree::kPixelFormatBGRA8)) { |
| return render_tree::kPixelFormatBGRA8; |
| } else { |
| return render_tree::kPixelFormatInvalid; |
| } |
| } |
| |
| // This functor/thread is designed to simply allocate many textures as quickly |
| // in a row as possible. Many threads should be capable of doing this |
| // simultaneously. |
| class CreateImagesThread : public base::SimpleThread { |
| public: |
| CreateImagesThread(ResourceProvider* resource_provider, |
| int num_images_to_create, const math::Size& image_size) |
| : base::SimpleThread("CreateImages"), |
| resource_provider_(resource_provider), |
| num_images_to_create_(num_images_to_create), |
| image_size_(image_size) {} |
| |
| void Run() override { |
| for (int i = 0; i < num_images_to_create_; ++i) { |
| std::unique_ptr<render_tree::ImageData> image_data = |
| resource_provider_->AllocateImageData( |
| image_size_, ChoosePixelFormat(resource_provider_), |
| render_tree::kAlphaFormatPremultiplied); |
| int num_bytes = image_data->GetDescriptor().pitch_in_bytes * |
| image_data->GetDescriptor().size.height(); |
| uint8* image_memory = image_data->GetMemory(); |
| for (int i = 0; i < num_bytes; ++i) { |
| image_memory[i] = 0; |
| } |
| resource_provider_->CreateImage(std::move(image_data)); |
| } |
| } |
| |
| private: |
| ResourceProvider* resource_provider_; |
| int num_images_to_create_; |
| const math::Size image_size_; |
| }; |
| |
| // This functor/thread will spawn the given number of CreateImagesThreads, |
| // execute them, and then wait for them to complete. Upon completion, it will |
| // call the provided callback function. |
| class CreateImagesSpawnerThread : public base::SimpleThread { |
| public: |
| CreateImagesSpawnerThread(ResourceProvider* resource_provider, |
| int threads_to_create, |
| int images_to_create_per_thread, |
| const math::Size& image_size, |
| base::Closure finished_callback) |
| : base::SimpleThread("CreateImagesSpawner"), |
| resource_provider_(resource_provider), |
| threads_to_create_(threads_to_create), |
| images_to_create_per_thread_(images_to_create_per_thread), |
| image_size_(image_size), |
| finished_callback_(finished_callback) {} |
| |
| void Run() override { |
| // First, create our threads. |
| typedef std::vector<CreateImagesThread*> ThreadVector; |
| ThreadVector image_creating_threads; |
| for (int i = 0; i < threads_to_create_; ++i) { |
| image_creating_threads.push_back(new CreateImagesThread( |
| resource_provider_, images_to_create_per_thread_, image_size_)); |
| } |
| |
| // Second, tell them all to begin executing at the same time. |
| for (ThreadVector::iterator iter = image_creating_threads.begin(); |
| iter != image_creating_threads.end(); ++iter) { |
| (*iter)->Start(); |
| } |
| |
| // Thirdly, wait for them to all finish executing and then destroy them. |
| for (ThreadVector::iterator iter = image_creating_threads.begin(); |
| iter != image_creating_threads.end(); ++iter) { |
| (*iter)->Join(); |
| delete *iter; |
| } |
| |
| finished_callback_.Run(); |
| } |
| |
| private: |
| ResourceProvider* resource_provider_; |
| int threads_to_create_; |
| int images_to_create_per_thread_; |
| const math::Size image_size_; |
| base::Closure finished_callback_; |
| }; |
| } // namespace |
| |
| class ResourceProviderTest : public testing::Test { |
| public: |
| ResourceProviderTest(); |
| ~ResourceProviderTest(); |
| |
| static void SetUpTestCase(); |
| static void TearDownTestCase(); |
| |
| // Lets the fixture know that it will run the run loop manually, and the |
| // ResourceProviderTest destructor does not need to run it. |
| void SetWillRunRunLoopManually() { run_run_loop_manually_ = true; } |
| |
| void Quit() { |
| message_loop_.task_runner()->PostTask(FROM_HERE, run_loop_.QuitClosure()); |
| } |
| |
| protected: |
| base::MessageLoop message_loop_; |
| base::RunLoop run_loop_; |
| std::unique_ptr<rasterizer::Rasterizer> rasterizer_; |
| |
| bool run_run_loop_manually_; |
| |
| static backend::GraphicsSystem* graphics_system_; |
| static backend::GraphicsContext* graphics_context_; |
| }; |
| |
| ResourceProviderTest::ResourceProviderTest() |
| : message_loop_(base::MessageLoop::TYPE_DEFAULT), |
| run_run_loop_manually_(false) { |
| // Create the rasterizer using the platform default RenderModule options. |
| RendererModule::Options render_module_options; |
| rasterizer_ = render_module_options.create_rasterizer_function.Run( |
| graphics_context_, render_module_options); |
| } |
| |
| ResourceProviderTest::~ResourceProviderTest() { |
| if (!run_run_loop_manually_) { |
| message_loop_.task_runner()->PostTask(FROM_HERE, run_loop_.QuitClosure()); |
| run_loop_.Run(); |
| } |
| } |
| |
| // static |
| backend::GraphicsSystem* ResourceProviderTest::graphics_system_ = nullptr; |
| // static |
| backend::GraphicsContext* ResourceProviderTest::graphics_context_ = nullptr; |
| |
| // static |
| void ResourceProviderTest::SetUpTestCase() { |
| graphics_system_ = backend::CreateDefaultGraphicsSystem().release(); |
| graphics_context_ = graphics_system_->CreateGraphicsContext().release(); |
| } |
| |
| // static |
| void ResourceProviderTest::TearDownTestCase() { |
| delete graphics_context_; |
| graphics_context_ = nullptr; |
| delete graphics_system_; |
| graphics_system_ = nullptr; |
| } |
| |
| // The following test ensures that any thread can successfully create render |
| // tree images both at the same time and also while a graphics frame is started. |
| // This might be a problem in an OpenGL implementation for instance if we |
| // are naively setting ourselves as the current OpenGL context in order to |
| // create a texture, in which case another thread holding the context would |
| // be upset that it was taken away. This test is essentially attempting to |
| // cause a crash or DCHECK() to be hit, if the test executes without crashing, |
| // everything is okay. |
| TEST_F(ResourceProviderTest, TexturesCanBeCreatedFromSecondaryThread) { |
| SetWillRunRunLoopManually(); |
| |
| // Create a dummy offscreen surface so that we can have a target when we start |
| // a frame with the graphics context. |
| const math::Size kDummySurfaceDimensions(1, 1); |
| scoped_refptr<backend::RenderTarget> dummy_output_surface = |
| graphics_context_->CreateOffscreenRenderTarget(kDummySurfaceDimensions); |
| |
| // Now that we're inside of a new frame, create images from a separate |
| // thread. This should be perfectly legal and cause no problems. |
| const int kNumThreads = 20; |
| const int kNumImagesCreatedPerThread = 300; |
| |
| // Create a thread to create other threads that will create images. |
| CreateImagesSpawnerThread spawner_thread( |
| rasterizer_->GetResourceProvider(), kNumThreads, |
| kNumImagesCreatedPerThread, math::Size(1, 1), |
| base::Bind(&ResourceProviderTest::Quit, base::Unretained(this))); |
| spawner_thread.Start(); |
| |
| // Run our message loop to process backend image creation/destruction |
| // requests. |
| run_loop_.Run(); |
| |
| spawner_thread.Join(); |
| } |
| |
| // If textures never get passed to the renderer, then both allocating and |
| // releasing them should happen instantly. In other words, this test ensures |
| // that we don't have a problem where texture allocations are instantaneous |
| // but texture deallocations are delayed. This might be a problem if the |
| // video decoder is tracking its video frame texture memory usage to maintain |
| // up to N textures in memory at a time. If it skips some frames, it might |
| // release them and then expect that it can allocate N more, but if those |
| // released frames have a delay on their release, we may have many more than |
| // N textures allocated at a time. |
| TEST_F(ResourceProviderTest, ManyTexturesCanBeCreatedAndDestroyedQuickly) { |
| SetWillRunRunLoopManually(); |
| |
| // Create a dummy offscreen surface so that we can have a target when we start |
| // a frame with the graphics context. |
| const math::Size kDummySurfaceDimensions(1, 1); |
| scoped_refptr<backend::RenderTarget> dummy_output_surface = |
| graphics_context_->CreateOffscreenRenderTarget(kDummySurfaceDimensions); |
| |
| // Now that we're inside of a new frame, create images from a separate |
| // thread. This should be perfectly legal and cause no problems. |
| const int kNumThreads = 2; |
| const int kNumImagesCreatedPerThread = 500; |
| |
| // Create a thread to create other threads that will create images. |
| CreateImagesSpawnerThread spawner_thread( |
| rasterizer_->GetResourceProvider(), kNumThreads, |
| kNumImagesCreatedPerThread, math::Size(256, 256), |
| base::Bind(&ResourceProviderTest::Quit, base::Unretained(this))); |
| spawner_thread.Start(); |
| |
| // Run our message loop to process backend image creation/destruction |
| // requests. |
| run_loop_.Run(); |
| |
| spawner_thread.Join(); |
| } |
| |
| // Test that we won't crash even if we attempt to create a very large number |
| // of textures. |
| TEST_F(ResourceProviderTest, NoCrashWhenManyTexturesAreAllocated) { |
| render_tree::ResourceProvider* resource_provider = |
| rasterizer_->GetResourceProvider(); |
| |
| const int kNumImages = 5000; |
| const math::Size kImageSize = math::Size(32, 32); |
| |
| std::unique_ptr<render_tree::ImageData> image_datas[kNumImages]; |
| scoped_refptr<render_tree::Image> images[kNumImages]; |
| |
| for (int i = 0; i < kNumImages; ++i) { |
| image_datas[i] = resource_provider->AllocateImageData( |
| kImageSize, ChoosePixelFormat(resource_provider), |
| render_tree::kAlphaFormatOpaque); |
| } |
| |
| for (int i = 0; i < kNumImages; ++i) { |
| if (image_datas[i]) { |
| images[i] = resource_provider->CreateImage(std::move(image_datas[i])); |
| } |
| } |
| |
| for (int i = 0; i < kNumImages; ++i) { |
| images[i] = NULL; |
| } |
| } |
| |
| // Test that we can attempt to allocate one massive image without crashing. |
| // There will likely be an out-of-memory issue when attempting to create the |
| // image. |
| TEST_F(ResourceProviderTest, NoCrashWhenMassiveTextureIsAllocated) { |
| render_tree::ResourceProvider* resource_provider = |
| rasterizer_->GetResourceProvider(); |
| |
| const math::Size kImageSize = math::Size(16384, 16384); |
| |
| std::unique_ptr<render_tree::ImageData> image_data = |
| resource_provider->AllocateImageData(kImageSize, |
| ChoosePixelFormat(resource_provider), |
| render_tree::kAlphaFormatOpaque); |
| if (image_data) { |
| scoped_refptr<render_tree::Image> image = |
| resource_provider->CreateImage(std::move(image_data)); |
| } |
| } |
| |
| // This test is likely not to fail for any out-of-memory reasons, but rather |
| // maximum texture size reasons. |
| TEST_F(ResourceProviderTest, |
| NoCrashWhenTextureWithOneLargeDimensionIsAllocated) { |
| render_tree::ResourceProvider* resource_provider = |
| rasterizer_->GetResourceProvider(); |
| |
| const math::Size kImageSize = math::Size(1024 * 1024, 32); |
| |
| std::unique_ptr<render_tree::ImageData> image_data = |
| resource_provider->AllocateImageData(kImageSize, |
| ChoosePixelFormat(resource_provider), |
| render_tree::kAlphaFormatOpaque); |
| if (image_data) { |
| scoped_refptr<render_tree::Image> image = |
| resource_provider->CreateImage(std::move(image_data)); |
| } |
| } |
| |
| } // namespace renderer |
| } // namespace cobalt |