/* * Copyright (C) 2020 The Android Open Source Project * * 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 #include #include #include #include #include #include #include #include #include #include using namespace utils; namespace filament { namespace viewer { static std::string gStatus; struct ScreenshotState { View* view; std::string filename; bool autoclose; AutomationEngine* engine; }; static void convertRGBAtoRGB(void* buffer, uint32_t width, uint32_t height) { uint8_t* writePtr = static_cast(buffer); uint8_t const* readPtr = static_cast(buffer); for (uint32_t i = 0, n = width * height; i < n; ++i) { writePtr[0] = readPtr[0]; writePtr[1] = readPtr[1]; writePtr[2] = readPtr[2]; writePtr += 3; readPtr += 4; } } static void exportScreenshot(View* view, Renderer* renderer, std::string filename, bool autoclose, AutomationEngine* automationEngine) { const Viewport& vp = view->getViewport(); const size_t byteCount = vp.width * vp.height * 4; // Create a buffer descriptor that writes the PPM after the data becomes ready on the CPU. backend::PixelBufferDescriptor buffer( new uint8_t[byteCount], byteCount, backend::PixelBufferDescriptor::PixelDataFormat::RGBA, backend::PixelBufferDescriptor::PixelDataType::UBYTE, [](void* buffer, size_t size, void* user) { ScreenshotState* state = static_cast(user); if (state->engine->isTerminated()) { delete[] static_cast(buffer); delete state; return; } const Viewport& vp = state->view->getViewport(); // ReadPixels on Metal only supports RGBA, but the PPM format only supports RGB. // So, manually perform a quick transformation here. convertRGBAtoRGB(buffer, vp.width, vp.height); Path out(state->filename); std::ofstream ppmStream(out); ppmStream << "P6 " << vp.width << " " << vp.height << " " << 255 << std::endl; ppmStream.write(static_cast(buffer), vp.width * vp.height * 3); delete[] static_cast(buffer); if (state->autoclose) { state->engine->requestClose(); } delete state; }, new ScreenshotState { view, filename, autoclose, automationEngine } ); // Invoke readPixels asynchronously. renderer->readPixels((uint32_t) vp.left, (uint32_t) vp.bottom, vp.width, vp.height, std::move(buffer)); } AutomationEngine* AutomationEngine::createFromJSON(const char* jsonSpec, size_t size) { AutomationSpec* spec = AutomationSpec::generate(jsonSpec, size); if (!spec) { return nullptr; } Settings* settings = new Settings(); AutomationEngine* result = new AutomationEngine(spec, settings); result->mOwnsSettings = true; return result; } AutomationEngine* AutomationEngine::createDefault() { AutomationSpec* spec = AutomationSpec::generateDefaultTestCases(); if (!spec) { return nullptr; } Settings* settings = new Settings(); AutomationEngine* result = new AutomationEngine(spec, settings); result->mOwnsSettings = true; return result; } AutomationEngine::~AutomationEngine() { if (mColorGrading) { mColorGradingEngine->destroy(mColorGrading); } if (mOwnsSettings) { delete mSpec; delete mSettings; } } void AutomationEngine::startRunning() { mRequestStart = true; } void AutomationEngine::startBatchMode() { mRequestStart = true; mBatchModeEnabled = true; } void AutomationEngine::terminate() { stopRunning(); mTerminated = true; } void AutomationEngine::exportSettings(const Settings& settings, const char* filename) { JsonSerializer serializer; std::string contents = serializer.writeJson(settings); std::ofstream out(filename); if (!out) { gStatus = "Failed to export settings file."; } out << contents << std::endl; gStatus = "Exported to '" + std::string(filename) + "' in the current folder."; } void AutomationEngine::applySettings(Engine* engine, const char* json, size_t jsonLength, const ViewerContent& content) { JsonSerializer serializer; if (!serializer.readJson(json, jsonLength, mSettings)) { std::string jsonWithTerminator(json, json + jsonLength); slog.e << "Badly formed JSON:\n" << jsonWithTerminator.c_str() << io::endl; return; } viewer::applySettings(engine, mSettings->view, content.view); for (size_t i = 0; i < content.materialCount; i++) { viewer::applySettings(engine, mSettings->material, content.materials[i]); } viewer::applySettings(engine, mSettings->lighting, content.indirectLight, content.sunlight, content.assetLights, content.assetLightCount, content.lightManager, content.scene, content.view); Camera* camera = &content.view->getCamera(); Skybox* skybox = content.scene->getSkybox(); viewer::applySettings(engine, mSettings->viewer, camera, skybox, content.renderer); } ColorGrading* AutomationEngine::getColorGrading(Engine* engine) { if (mSettings->view.colorGrading != mColorGradingSettings) { mColorGradingSettings = mSettings->view.colorGrading; if (mColorGrading) { mColorGradingEngine->destroy(mColorGrading); } mColorGrading = createColorGrading(mColorGradingSettings, engine); mColorGradingEngine = engine; } return mColorGrading; } ViewerOptions AutomationEngine::getViewerOptions() const { ViewerOptions options = mSettings->viewer; const auto dofOptions = mSettings->view.dof; if (dofOptions.enabled) { options.cameraFocalLength = Camera::computeEffectiveFocalLength( options.cameraFocalLength / 1000.0, std::max(0.1f, options.cameraFocusDistance)) * 1000.0; } return mSettings->viewer; } void AutomationEngine::tick(Engine* engine, const ViewerContent& content, float deltaTime) { const auto activateTest = [this, engine, content]() { mElapsedTime = 0; mElapsedFrames = 0; mSpec->get(mCurrentTest, mSettings); viewer::applySettings(engine, mSettings->view, content.view); for (size_t i = 0; i < content.materialCount; i++) { viewer::applySettings(engine, mSettings->material, content.materials[i]); } if (mOptions.verbose) { utils::slog.i << "Running test " << mCurrentTest << utils::io::endl; } }; if (!mIsRunning) { if (mRequestStart) { if ((mBatchModeEnabled && mBatchModeAllowed) || !mBatchModeEnabled) { mIsRunning = true; mRequestStart = false; mCurrentTest = 0; activateTest(); } } return; } mElapsedTime += deltaTime; mElapsedFrames++; if (mElapsedTime < mOptions.sleepDuration || mElapsedFrames < mOptions.minFrameCount) { return; } const bool isLastTest = mCurrentTest == mSpec->size() - 1; const int digits = (int) log10 ((double) mSpec->size()) + 1; std::ostringstream stringStream; stringStream << mSpec->getName(mCurrentTest) << std::setfill('0') << std::setw(digits) << mCurrentTest; std::string prefix = stringStream.str(); if (mOptions.exportSettings) { std::string filename = prefix + ".json"; exportSettings(*mSettings, filename.c_str()); } if (mOptions.exportScreenshots) { exportScreenshot(content.view, content.renderer, prefix + ".ppm", isLastTest, this); } if (isLastTest) { mIsRunning = false; if (mBatchModeEnabled && !mOptions.exportScreenshots) { mShouldClose = true; } return; } // Increment the case number and apply the next round of settings. mCurrentTest++; activateTest(); } const char* AutomationEngine::getStatusMessage() const { return gStatus.c_str(); } } // namespace viewer } // namespace filament