Compare commits
5 Commits
bjd/add-mi
...
rc/1.68.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
474a4f3fcd | ||
|
|
d4efef9a9b | ||
|
|
cdffc9eaa0 | ||
|
|
a162a65dce | ||
|
|
369bab4744 |
@@ -31,7 +31,7 @@ repositories {
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.google.android.filament:filament-android:1.68.0'
|
||||
implementation 'com.google.android.filament:filament-android:1.68.2'
|
||||
}
|
||||
```
|
||||
|
||||
@@ -51,7 +51,7 @@ Here are all the libraries available in the group `com.google.android.filament`:
|
||||
iOS projects can use CocoaPods to install the latest release:
|
||||
|
||||
```shell
|
||||
pod 'Filament', '~> 1.68.0'
|
||||
pod 'Filament', '~> 1.68.2'
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -7,6 +7,10 @@ A new header is inserted each time a *tag* is created.
|
||||
Instead, if you are authoring a PR for the main branch, add your release note to
|
||||
[NEW_RELEASE_NOTES.md](./NEW_RELEASE_NOTES.md).
|
||||
|
||||
## v1.68.2
|
||||
|
||||
- Support `setPresentationTime` with the Metal backend.
|
||||
|
||||
## v1.68.1
|
||||
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
GROUP=com.google.android.filament
|
||||
VERSION_NAME=1.68.0
|
||||
VERSION_NAME=1.68.2
|
||||
|
||||
POM_DESCRIPTION=Real-time physically based rendering engine for Android.
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ set(SRCS
|
||||
src/Driver.cpp
|
||||
src/Handle.cpp
|
||||
src/HandleAllocator.cpp
|
||||
src/JobQueue.cpp
|
||||
src/ostream.cpp
|
||||
src/noop/NoopDriver.cpp
|
||||
src/noop/PlatformNoop.cpp
|
||||
@@ -59,6 +60,7 @@ set(PRIVATE_HDRS
|
||||
src/CompilerThreadPool.h
|
||||
src/DataReshaper.h
|
||||
src/DriverBase.h
|
||||
src/JobQueue.h
|
||||
)
|
||||
|
||||
# ==================================================================================================
|
||||
@@ -568,6 +570,7 @@ if (APPLE OR LINUX)
|
||||
test/test_ReadPixels.cpp
|
||||
test/test_BufferUpdates.cpp
|
||||
test/test_Callbacks.cpp
|
||||
test/test_JobQueue.cpp
|
||||
test/test_MemoryMappedBuffer.cpp
|
||||
test/test_MsaaSwapChain.cpp
|
||||
test/test_MRT.cpp
|
||||
|
||||
247
filament/backend/src/JobQueue.cpp
Normal file
247
filament/backend/src/JobQueue.cpp
Normal file
@@ -0,0 +1,247 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 "JobQueue.h"
|
||||
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/debug.h>
|
||||
#include <utils/Panic.h>
|
||||
|
||||
namespace filament::backend {
|
||||
|
||||
JobQueue::JobQueue(PassKey) {}
|
||||
|
||||
JobQueue::JobId JobQueue::push(Job job, JobId const preIssuedJobId/* = InvalidJobId*/) {
|
||||
JobId jobId = preIssuedJobId;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mQueueMutex);
|
||||
if (mIsStopping) {
|
||||
return InvalidJobId;
|
||||
}
|
||||
|
||||
if (jobId == InvalidJobId) {
|
||||
jobId = genNextJobId();
|
||||
mJobsMap[jobId] = std::move(job);
|
||||
} else {
|
||||
// Use the job ID previously issued by `issueJobId()`
|
||||
auto it = mJobsMap.find(jobId);
|
||||
if (it == mJobsMap.end()) {
|
||||
// Pre-issued job does not exist, either users passed a wrong id (unlikely)
|
||||
// or the job must have been canceled (likely)
|
||||
return InvalidJobId;
|
||||
}
|
||||
FILAMENT_CHECK_PRECONDITION(!static_cast<bool>(it->second))
|
||||
<< "pre-issued job has already been populated";
|
||||
it->second = std::move(job);
|
||||
}
|
||||
mJobOrder.push(jobId);
|
||||
}
|
||||
|
||||
// Always notify. A ThreadWorker might be waiting.
|
||||
mQueueCondition.notify_one();
|
||||
|
||||
return jobId;
|
||||
}
|
||||
|
||||
JobQueue::Job JobQueue::pop(bool shouldBlock) {
|
||||
std::unique_lock<std::mutex> lock(mQueueMutex);
|
||||
|
||||
decltype(mJobsMap)::iterator it;
|
||||
|
||||
while (true) {
|
||||
if (shouldBlock) {
|
||||
// Wait only if we're in blocking mode and the queue is empty
|
||||
mQueueCondition.wait(lock, [this] { return !mJobOrder.empty() || mIsStopping; });
|
||||
}
|
||||
|
||||
if (mJobOrder.empty()) {
|
||||
// When `shouldBlock` is true, this means the queue is stopping now.
|
||||
// When `shouldBlock` is false, this means there's no job.
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
JobId jobId = mJobOrder.front();
|
||||
mJobOrder.pop();
|
||||
|
||||
it = mJobsMap.find(jobId);
|
||||
if (it != mJobsMap.end()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// If execution reaches this line, the job must have been canceled right after being added.
|
||||
// Therefore, we should continue the loop and attempt to retrieve the next available job.
|
||||
}
|
||||
|
||||
Job job = std::move(it->second);
|
||||
mJobsMap.erase(it);
|
||||
return job;
|
||||
}
|
||||
|
||||
utils::FixedCapacityVector<JobQueue::Job> JobQueue::popBatch(int const maxJobsToPop) {
|
||||
utils::FixedCapacityVector<Job> jobs;
|
||||
|
||||
if (UTILS_UNLIKELY(maxJobsToPop == 0)) {
|
||||
return jobs;
|
||||
}
|
||||
|
||||
std::lock_guard<std::mutex> lock(mQueueMutex);
|
||||
if (mJobOrder.empty()) {
|
||||
return jobs;
|
||||
}
|
||||
|
||||
// Calculate jobs to take. If maxJobsToPop is negative, we take all jobs.
|
||||
size_t jobsToTake = mJobOrder.size();
|
||||
if (0 < maxJobsToPop && maxJobsToPop < static_cast<int>(jobsToTake)) {
|
||||
jobsToTake = maxJobsToPop;
|
||||
}
|
||||
jobs.reserve(jobsToTake);
|
||||
|
||||
while (0 < jobsToTake && !mJobOrder.empty()) {
|
||||
JobId jobId = mJobOrder.front();
|
||||
mJobOrder.pop();
|
||||
|
||||
auto it = mJobsMap.find(jobId);
|
||||
if (UTILS_UNLIKELY(it == mJobsMap.end())) {
|
||||
// The job was probably canceled.
|
||||
continue;
|
||||
}
|
||||
|
||||
jobs.push_back(std::move(it->second));
|
||||
--jobsToTake;
|
||||
mJobsMap.erase(it);
|
||||
}
|
||||
|
||||
return jobs;
|
||||
}
|
||||
|
||||
JobQueue::JobId JobQueue::issueJobId() noexcept {
|
||||
std::lock_guard<std::mutex> lock(mQueueMutex);
|
||||
JobId const jobId = genNextJobId();
|
||||
// Preallocate a job, which serves two main purposes. It provides a valid jobId that can be
|
||||
// checked for integrity when passed to the `push` method, and it enables job cancellation for
|
||||
// tasks that are yet to be pushed.
|
||||
mJobsMap[jobId];
|
||||
return jobId;
|
||||
}
|
||||
|
||||
bool JobQueue::cancel(JobId const jobId) noexcept {
|
||||
std::lock_guard<std::mutex> lock(mQueueMutex);
|
||||
|
||||
auto it = mJobsMap.find(jobId);
|
||||
if (it == mJobsMap.end()) {
|
||||
return false; // Job not found, must have been completed or canceled.
|
||||
}
|
||||
|
||||
mJobsMap.erase(it);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void JobQueue::stop() noexcept {
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(mQueueMutex);
|
||||
mIsStopping = true;
|
||||
}
|
||||
mQueueCondition.notify_all(); // Wake up all waiting threads
|
||||
}
|
||||
|
||||
JobQueue::JobId JobQueue::genNextJobId() noexcept {
|
||||
// We assume this method is called within the critical section.
|
||||
JobId newJobId = mNextJobId++;
|
||||
// We assume the job ID won't overflow or wraps around to zero within the application's lifetime.
|
||||
assert_invariant(newJobId != InvalidJobId);
|
||||
return newJobId;
|
||||
}
|
||||
|
||||
JobWorker::~JobWorker() = default;
|
||||
|
||||
void JobWorker::terminate() {
|
||||
// This is called from workers `terminate()`, which may hinder the concurrent use of multiple
|
||||
// workers. Consider removing this line and require the owner/caller to explicitly invoke it to
|
||||
// enable multiple worker instances.
|
||||
if (mQueue) {
|
||||
mQueue->stop();
|
||||
}
|
||||
}
|
||||
|
||||
AmortizationWorker::AmortizationWorker(JobQueue::Ptr queue, PassKey)
|
||||
: JobWorker(std::move(queue)) {
|
||||
}
|
||||
|
||||
AmortizationWorker::~AmortizationWorker() = default;
|
||||
|
||||
void AmortizationWorker::process(int const jobCount) {
|
||||
if (!mQueue || jobCount == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (jobCount == 1) {
|
||||
// Handle single job without vector allocation.
|
||||
if (auto job = mQueue->pop(false)) {
|
||||
job();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle batch (jobCount > 1 or jobCount < 0 for "all pending jobs")
|
||||
utils::FixedCapacityVector<JobQueue::Job> jobs = mQueue->popBatch(jobCount);
|
||||
if (jobs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (auto& job: jobs) {
|
||||
job();
|
||||
}
|
||||
}
|
||||
|
||||
void AmortizationWorker::terminate() {
|
||||
JobWorker::terminate();
|
||||
|
||||
// Drain all pending jobs.
|
||||
process(-1);
|
||||
}
|
||||
|
||||
ThreadWorker::ThreadWorker(JobQueue::Ptr queue, Config config, PassKey)
|
||||
: JobWorker(std::move(queue)), mConfig(std::move(config)) {
|
||||
mThread = std::thread([this]() {
|
||||
utils::JobSystem::setThreadName(mConfig.name.data());
|
||||
utils::JobSystem::setThreadPriority(mConfig.priority);
|
||||
|
||||
if (mConfig.onBegin) {
|
||||
mConfig.onBegin();
|
||||
}
|
||||
|
||||
while (JobQueue::Job job = mQueue->pop(true)) {
|
||||
job();
|
||||
}
|
||||
|
||||
if (mConfig.onEnd) {
|
||||
mConfig.onEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ThreadWorker::~ThreadWorker() = default;
|
||||
|
||||
void ThreadWorker::terminate() {
|
||||
JobWorker::terminate();
|
||||
|
||||
if (mThread.joinable()) {
|
||||
mThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace utils
|
||||
275
filament/backend/src/JobQueue.h
Normal file
275
filament/backend/src/JobQueue.h
Normal file
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.
|
||||
*/
|
||||
|
||||
#ifndef TNT_FILAMENT_BACKEND_PRIVATE_JOBQUEUE_H
|
||||
#define TNT_FILAMENT_BACKEND_PRIVATE_JOBQUEUE_H
|
||||
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
#include <utils/Invocable.h>
|
||||
#include <utils/JobSystem.h>
|
||||
|
||||
#include <mutex>
|
||||
#include <condition_variable>
|
||||
#include <thread>
|
||||
#include <memory>
|
||||
#include <unordered_map>
|
||||
#include <limits>
|
||||
#include <queue>
|
||||
|
||||
namespace filament::backend {
|
||||
|
||||
/**
|
||||
* A thread-safe producer-consumer queue with batching capabilities.
|
||||
*
|
||||
* This class is thread-safe. All public methods can be called from any thread.
|
||||
*
|
||||
* This class is stateless regarding concurrency. The *caller* decides the blocking behavior and/or
|
||||
* batching when they call a 'pop' methods.
|
||||
*
|
||||
* A typical use case looks like this:
|
||||
*
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* #include "private/backend/JobQueue.h"
|
||||
* using namespace filament::backend;
|
||||
*
|
||||
* JobQueue::Ptr queue = JobQueue::create();
|
||||
* JobWorker::Ptr worker = AmortizationWorker::create(queue);
|
||||
* [ or JobWorker::Ptr worker = ThreadWorker::create(queue, config); ]
|
||||
*
|
||||
* void loop() {
|
||||
* worker->process(2); // for AmortizationWorker
|
||||
* }
|
||||
*
|
||||
* void cleanup() {
|
||||
* worker->terminate();
|
||||
* }
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* JobId id = queue->push([](){ ... });
|
||||
* queue->cancel(id);
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
* JobId preIssuedId = queue->issueJobId();
|
||||
* JobId id = queue->push([](){ ... }, preIssuedId);
|
||||
* assert(id == preIssuedId);
|
||||
* ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
*/
|
||||
class JobQueue {
|
||||
struct PassKey {};
|
||||
public:
|
||||
using Job = utils::Invocable<void()>;
|
||||
using JobId = uint32_t;
|
||||
using Ptr = std::shared_ptr<JobQueue>;
|
||||
static constexpr JobId InvalidJobId = std::numeric_limits<JobId>::max();
|
||||
|
||||
/**
|
||||
* Creates an instance of JobQueue. Users should call this to create one.
|
||||
* @return An instance of JobQueue
|
||||
*/
|
||||
static Ptr create() {
|
||||
return std::make_shared<JobQueue>(PassKey{});
|
||||
}
|
||||
|
||||
explicit JobQueue(PassKey); // This can be created only via `create()`
|
||||
|
||||
/**
|
||||
* Pushes a new job into queue.
|
||||
*
|
||||
* If the queue is in the process of shutting down (via a call to `stop`), this method does
|
||||
* nothing (a no-op) and returns an invalid job ID.
|
||||
*
|
||||
* @param job The function/lambda to be executed.
|
||||
* @param preIssuedJobId The previously issued job ID where this job is assigned to.
|
||||
* If the value is `InvalidJobId`, a new job ID is generated internally.
|
||||
* @return A new job ID. If a `preIssuedJobId` is provided, that specific ID is returned instead.
|
||||
*/
|
||||
JobId push(Job job, JobId preIssuedJobId = InvalidJobId);
|
||||
|
||||
/**
|
||||
* Retrieves the next job from queue.
|
||||
*
|
||||
* This method returns a valid job ID if there are pending jobs in the queue, even if the queue
|
||||
* is currently in the process of shutting down (via a call to `stop`).
|
||||
*
|
||||
* @param shouldBlock If true (typically used by ThreadWorker), waits for a job and returns it.
|
||||
* If false (typically used by AmortizationWorker), tries retrieving a job. But may return an
|
||||
* empty job if there's no pending job.
|
||||
* @return The next job. If an empty job is returned, it has different meaning depending on the
|
||||
* `shouldBlock` value:
|
||||
* - true: Stop processing (shutting down).
|
||||
* - false: No jobs currently in queue.
|
||||
*/
|
||||
Job pop(bool shouldBlock);
|
||||
|
||||
/**
|
||||
* Retrieves a batch of next jobs from queue. Always non-blocking.
|
||||
*
|
||||
* This method returns a valid job ID if there are pending jobs in the queue, even if the queue
|
||||
* is currently in the process of shutting down (via a call to `stop`).
|
||||
*
|
||||
* @param maxJobsToPop The maximum number of jobs to retrieve.
|
||||
* If < 0, retrieves all pending jobs.
|
||||
* @return A FixedCapacityVector<Job> containing the retrieved jobs.
|
||||
* Returns an empty vector if the queue is empty.
|
||||
*/
|
||||
utils::FixedCapacityVector<Job> popBatch(int maxJobsToPop);
|
||||
|
||||
/**
|
||||
* Generate a new job ID. This newly generated ID is meant to be used for the `preIssuedJobId`
|
||||
* parameter of `push` method.
|
||||
*
|
||||
* @return A new job ID.
|
||||
*/
|
||||
JobId issueJobId() noexcept;
|
||||
|
||||
/**
|
||||
* Cancels a job by its ID.
|
||||
*
|
||||
* @param jobId The job ID to cancel.
|
||||
* @return true if the job was found and cancelled, false otherwise.
|
||||
*/
|
||||
bool cancel(JobId jobId) noexcept;
|
||||
|
||||
/**
|
||||
* Signals the queue to shut down, after which no further jobs can be added using the `push`
|
||||
* method. but all jobs already pushed can still be processed using `pop`.
|
||||
*/
|
||||
void stop() noexcept;
|
||||
|
||||
private:
|
||||
JobQueue(const JobQueue&) = delete;
|
||||
JobQueue& operator=(const JobQueue&) = delete;
|
||||
|
||||
JobId genNextJobId() noexcept;
|
||||
|
||||
std::mutex mQueueMutex;
|
||||
std::condition_variable mQueueCondition;
|
||||
std::unordered_map<JobId, Job> mJobsMap;
|
||||
std::queue<JobId> mJobOrder;
|
||||
JobId mNextJobId = 0;
|
||||
bool mIsStopping = false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Abstract base class for all worker types.
|
||||
*/
|
||||
class JobWorker {
|
||||
public:
|
||||
using Ptr = std::unique_ptr<JobWorker>;
|
||||
|
||||
virtual ~JobWorker();
|
||||
|
||||
/**
|
||||
* Processes a batch of jobs. (For non-threaded workers)
|
||||
* @param jobCount Max jobs to process (<= 0 for all).
|
||||
*/
|
||||
virtual void process(int jobCount) {}
|
||||
|
||||
/**
|
||||
* Terminates the worker.
|
||||
*/
|
||||
virtual void terminate();
|
||||
|
||||
protected:
|
||||
explicit JobWorker(JobQueue::Ptr queue) : mQueue(std::move(queue)) {}
|
||||
|
||||
JobQueue::Ptr mQueue;
|
||||
|
||||
private:
|
||||
JobWorker(const JobWorker&) = delete;
|
||||
JobWorker& operator=(const JobWorker&) = delete;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A non-threaded worker that consumes jobs in batches.
|
||||
*/
|
||||
class AmortizationWorker final : public JobWorker {
|
||||
struct PassKey {};
|
||||
public:
|
||||
/**
|
||||
* Creates an instance of AmortizationWorker. Users should call this to create one.
|
||||
* @return An instance of AmortizationWorker
|
||||
*/
|
||||
static Ptr create(JobQueue::Ptr queue) {
|
||||
return std::make_unique<AmortizationWorker>(std::move(queue), PassKey{});
|
||||
}
|
||||
|
||||
explicit AmortizationWorker(JobQueue::Ptr queue, PassKey); // This can be created only via `create()`
|
||||
|
||||
~AmortizationWorker() override;
|
||||
|
||||
/**
|
||||
* Polls the queue and executes a batch of jobs.
|
||||
*
|
||||
* @param jobCount The max number of jobs to process.
|
||||
* 0 = do nothing.
|
||||
* 1 = pop one (optimized).
|
||||
* > 1 = pop batch.
|
||||
* <= -1 = pop all.
|
||||
*/
|
||||
void process(int jobCount) override;
|
||||
|
||||
/**
|
||||
* Signals the queue to stop and drain all pending jobs.
|
||||
* This is safe to call multiple times.
|
||||
*/
|
||||
void terminate() override;
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* A threaded worker that consumes jobs one by one, blocking when empty.
|
||||
*/
|
||||
class ThreadWorker final : public JobWorker {
|
||||
struct PassKey {};
|
||||
public:
|
||||
using Priority = utils::JobSystem::Priority;
|
||||
|
||||
/**
|
||||
* Config settings for the worker
|
||||
*/
|
||||
struct Config {
|
||||
std::string_view name = "";
|
||||
Priority priority = Priority::NORMAL;
|
||||
utils::Invocable<void()> onBegin; // Executed when the thread worker begins
|
||||
utils::Invocable<void()> onEnd; // Executed when the thread worker ends
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an instance of ThreadWorker. Users should call this to create one.
|
||||
* @return An instance of ThreadWorker
|
||||
*/
|
||||
static Ptr create(JobQueue::Ptr queue, Config config) {
|
||||
return std::make_unique<ThreadWorker>(std::move(queue), std::move(config), PassKey{});
|
||||
}
|
||||
|
||||
ThreadWorker(JobQueue::Ptr queue, Config config, PassKey); // This can be created only via `create()`
|
||||
|
||||
~ThreadWorker() override;
|
||||
|
||||
/**
|
||||
* Signals the queue to stop and joins the worker thread.
|
||||
* This is safe to call multiple times.
|
||||
*/
|
||||
void terminate() override;
|
||||
|
||||
private:
|
||||
Config mConfig;
|
||||
std::thread mThread;
|
||||
};
|
||||
|
||||
} // namespace filament::backend
|
||||
|
||||
#endif // TNT_FILAMENT_BACKEND_PRIVATE_JOBQUEUE_H
|
||||
@@ -312,6 +312,8 @@ void MetalDriver::execute(std::function<void(void)> const& fn) noexcept {
|
||||
}
|
||||
|
||||
void MetalDriver::setPresentationTime(int64_t monotonic_clock_ns) {
|
||||
assert_invariant(mContext->currentDrawSwapChain);
|
||||
mContext->currentDrawSwapChain->setPresentationTime(monotonic_clock_ns);
|
||||
}
|
||||
|
||||
void MetalDriver::endFrame(uint32_t frameId) {
|
||||
|
||||
@@ -141,6 +141,8 @@ public:
|
||||
// FrameScheduledCallback.
|
||||
void present();
|
||||
|
||||
void setPresentationTime(int64_t timeNs) { presentationTimeNs = timeNs; }
|
||||
|
||||
NSUInteger getSurfaceWidth() const;
|
||||
NSUInteger getSurfaceHeight() const;
|
||||
NSUInteger getSampleCount() const;
|
||||
@@ -159,7 +161,7 @@ private:
|
||||
bool isCaMetalLayer() const { return type == SwapChainType::CAMETALLAYER; }
|
||||
bool isHeadless() const { return type == SwapChainType::HEADLESS; }
|
||||
|
||||
void scheduleFrameScheduledCallback();
|
||||
void scheduleFrameScheduledCallback(int64_t presentationTimeNs);
|
||||
void scheduleFrameCompletedCallback();
|
||||
|
||||
MetalAttachment acquireBaseDrawable();
|
||||
@@ -192,6 +194,11 @@ private:
|
||||
|
||||
int64_t abandonedUntilFrame = -1;
|
||||
|
||||
// If zero, the next presentation should happen as soon as possible.
|
||||
// Otherwise, this is the timestamp when the present should happen.
|
||||
// Resets to 0 after the present.
|
||||
int64_t presentationTimeNs = 0;
|
||||
|
||||
// These fields store a callback to notify the client that a frame is ready for presentation. If
|
||||
// !frameScheduled.callback, then the Metal backend automatically calls presentDrawable when the
|
||||
// frame is committed. Otherwise, the Metal backend will not automatically present the frame.
|
||||
|
||||
@@ -325,11 +325,19 @@ void MetalSwapChain::present() {
|
||||
if (frameCompleted.callback) {
|
||||
scheduleFrameCompletedCallback();
|
||||
}
|
||||
const auto timeNs = presentationTimeNs;
|
||||
presentationTimeNs = 0;
|
||||
if (drawable) {
|
||||
if (frameScheduled.callback) {
|
||||
scheduleFrameScheduledCallback();
|
||||
scheduleFrameScheduledCallback(timeNs);
|
||||
} else {
|
||||
[getPendingCommandBuffer(&context) presentDrawable:drawable];
|
||||
if (presentationTimeNs) {
|
||||
const CFTimeInterval timeSeconds =
|
||||
(CFTimeInterval) presentationTimeNs / 1000000000.0;
|
||||
[getPendingCommandBuffer(&context) presentDrawable:drawable atTime:timeSeconds];
|
||||
} else {
|
||||
[getPendingCommandBuffer(&context) presentDrawable:drawable];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -341,15 +349,22 @@ public:
|
||||
PresentDrawableData& operator=(const PresentDrawableData&) = delete;
|
||||
|
||||
static PresentDrawableData* create(id<CAMetalDrawable> drawable,
|
||||
std::shared_ptr<std::mutex> drawableMutex, MetalDriver* driver, uint64_t flags) {
|
||||
std::shared_ptr<std::mutex> drawableMutex, MetalDriver* driver, uint64_t flags,
|
||||
int64_t presentationTimeNs) {
|
||||
assert_invariant(drawableMutex);
|
||||
assert_invariant(driver);
|
||||
return new PresentDrawableData(drawable, drawableMutex, driver, flags);
|
||||
return new PresentDrawableData(drawable, drawableMutex, driver, flags, presentationTimeNs);
|
||||
}
|
||||
|
||||
static void maybePresentAndDestroyAsync(PresentDrawableData* that, bool shouldPresent) {
|
||||
if (shouldPresent) {
|
||||
[that->mDrawable present];
|
||||
if (that->mPresentationTimeNs) {
|
||||
const CFTimeInterval timeSeconds =
|
||||
(CFTimeInterval) that->mPresentationTimeNs / 1000000000.0;
|
||||
[that->mDrawable presentAtTime:timeSeconds];
|
||||
} else {
|
||||
[that->mDrawable present];
|
||||
}
|
||||
}
|
||||
|
||||
if (that->mFlags & SwapChain::CALLBACK_DEFAULT_USE_METAL_COMPLETION_HANDLER) {
|
||||
@@ -367,8 +382,12 @@ public:
|
||||
|
||||
private:
|
||||
PresentDrawableData(id<CAMetalDrawable> drawable, std::shared_ptr<std::mutex> drawableMutex,
|
||||
MetalDriver* driver, uint64_t flags)
|
||||
: mDrawable(drawable), mDrawableMutex(drawableMutex), mDriver(driver), mFlags(flags) {}
|
||||
MetalDriver* driver, uint64_t flags, int64_t presentationTimeNs)
|
||||
: mDrawable(drawable),
|
||||
mDrawableMutex(drawableMutex),
|
||||
mDriver(driver),
|
||||
mFlags(flags),
|
||||
mPresentationTimeNs(presentationTimeNs) {}
|
||||
|
||||
static void cleanupAndDestroy(PresentDrawableData *that) {
|
||||
if (that->mDrawable) {
|
||||
@@ -384,6 +403,7 @@ private:
|
||||
std::shared_ptr<std::mutex> mDrawableMutex;
|
||||
MetalDriver* mDriver = nullptr;
|
||||
uint64_t mFlags = 0;
|
||||
int64_t mPresentationTimeNs = 0;
|
||||
};
|
||||
|
||||
void presentDrawable(bool presentFrame, void* user) {
|
||||
@@ -391,7 +411,7 @@ void presentDrawable(bool presentFrame, void* user) {
|
||||
PresentDrawableData::maybePresentAndDestroyAsync(presentDrawableData, presentFrame);
|
||||
}
|
||||
|
||||
void MetalSwapChain::scheduleFrameScheduledCallback() {
|
||||
void MetalSwapChain::scheduleFrameScheduledCallback(int64_t presentationTimeNs) {
|
||||
if (!frameScheduled.callback) {
|
||||
return;
|
||||
}
|
||||
@@ -400,8 +420,11 @@ void MetalSwapChain::scheduleFrameScheduledCallback() {
|
||||
|
||||
struct Callback {
|
||||
Callback(std::shared_ptr<FrameScheduledCallback> callback, id<CAMetalDrawable> drawable,
|
||||
std::shared_ptr<std::mutex> drawableMutex, MetalDriver* driver, uint64_t flags)
|
||||
: f(callback), data(PresentDrawableData::create(drawable, drawableMutex, driver, flags)) {}
|
||||
std::shared_ptr<std::mutex> drawableMutex, MetalDriver* driver, uint64_t flags,
|
||||
int64_t presentationTimeNs)
|
||||
: f(callback),
|
||||
data(PresentDrawableData::create(drawable, drawableMutex, driver, flags,
|
||||
presentationTimeNs)) {}
|
||||
std::shared_ptr<FrameScheduledCallback> f;
|
||||
// PresentDrawableData* is destroyed by maybePresentAndDestroyAsync() later.
|
||||
std::unique_ptr<PresentDrawableData> data;
|
||||
@@ -419,7 +442,7 @@ void MetalSwapChain::scheduleFrameScheduledCallback() {
|
||||
uint64_t const flags = frameScheduled.flags;
|
||||
ReleasablePointer* callback = [[ReleasablePointer alloc]
|
||||
initWithPointer:new Callback(frameScheduled.callback, drawable, layerDrawableMutex,
|
||||
context.driver, flags)];
|
||||
context.driver, flags, presentationTimeNs)];
|
||||
|
||||
backend::CallbackHandler* handler = frameScheduled.handler;
|
||||
MetalDriver* driver = context.driver;
|
||||
|
||||
288
filament/backend/test/test_JobQueue.cpp
Normal file
288
filament/backend/test/test_JobQueue.cpp
Normal file
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 <gtest/gtest.h>
|
||||
|
||||
#include "JobQueue.h"
|
||||
|
||||
#include <atomic>
|
||||
#include <thread>
|
||||
|
||||
using namespace filament::backend;
|
||||
|
||||
TEST(JobQueue, PushAndPop) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v = 1; });
|
||||
JobQueue::Job job = queue->pop(false);
|
||||
ASSERT_TRUE(job);
|
||||
job();
|
||||
EXPECT_EQ(1, v);
|
||||
}
|
||||
|
||||
TEST(JobQueue, PopEmpty) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobQueue::Job job = queue->pop(false);
|
||||
ASSERT_FALSE(job);
|
||||
}
|
||||
|
||||
TEST(JobQueue, PopBatch) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
auto jobs = queue->popBatch(2);
|
||||
EXPECT_EQ(2, jobs.size());
|
||||
for (auto& job : jobs) {
|
||||
job();
|
||||
}
|
||||
EXPECT_EQ(2, v);
|
||||
|
||||
jobs = queue->popBatch(10);
|
||||
EXPECT_EQ(1, jobs.size());
|
||||
for (auto& job : jobs) {
|
||||
job();
|
||||
}
|
||||
EXPECT_EQ(3, v);
|
||||
}
|
||||
|
||||
TEST(JobQueue, PopAll) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
auto jobs = queue->popBatch(-1);
|
||||
EXPECT_EQ(3, jobs.size());
|
||||
for (auto& job : jobs) {
|
||||
job();
|
||||
}
|
||||
EXPECT_EQ(3, v);
|
||||
}
|
||||
|
||||
TEST(JobQueue, Cancel) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
int v = 0;
|
||||
|
||||
JobQueue::JobId idToCancel = queue->push([&v]() { v = 1; });
|
||||
queue->push([&v]() { v = 2; });
|
||||
|
||||
EXPECT_TRUE(queue->cancel(idToCancel));
|
||||
|
||||
auto jobs = queue->popBatch(-1);
|
||||
EXPECT_EQ(1, jobs.size());
|
||||
jobs[0]();
|
||||
EXPECT_EQ(2, v);
|
||||
}
|
||||
|
||||
TEST(JobQueue, CancelInvalid) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
EXPECT_FALSE(queue->cancel(123));
|
||||
}
|
||||
|
||||
TEST(JobQueue, Stop) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
int v = 0;
|
||||
queue->push([&v]() { v = 1; });
|
||||
queue->stop();
|
||||
// After stop, we can't push new jobs. This should be a no-op.
|
||||
JobQueue::JobId id = queue->push([&v]() { v = 2; });
|
||||
EXPECT_EQ(JobQueue::InvalidJobId, id);
|
||||
|
||||
auto job = queue->pop(false);
|
||||
EXPECT_TRUE(job);
|
||||
job();
|
||||
EXPECT_EQ(1, v);
|
||||
|
||||
job = queue->pop(false);
|
||||
EXPECT_FALSE(job);
|
||||
}
|
||||
|
||||
TEST(JobQueue, PreIssuedJobId) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobQueue::JobId preIssuedId = queue->issueJobId();
|
||||
JobQueue::JobId id = queue->push([]() {}, preIssuedId);
|
||||
EXPECT_EQ(id, preIssuedId);
|
||||
}
|
||||
|
||||
TEST(JobQueue, MultipleProducersConsumers) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
std::atomic_int v = {0};
|
||||
constexpr int NUM_THREADS = 4;
|
||||
constexpr int JOBS_PER_THREAD = 200;
|
||||
|
||||
// Multiple producers
|
||||
std::vector<std::thread> producers;
|
||||
std::atomic_bool doneProducing = false;
|
||||
for (int i = 0; i < NUM_THREADS; ++i) {
|
||||
producers.emplace_back([&]() {
|
||||
for (int j = 0; j < JOBS_PER_THREAD; ++j) {
|
||||
queue->push([&v]() { v++; });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Multiple consumers
|
||||
std::thread blockingConsumer = std::thread([&]() {
|
||||
while (true) {
|
||||
if (auto job = queue->pop(true)) {
|
||||
job();
|
||||
} else {
|
||||
break; // This means the job queue is stopped.
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread nonBlockingConsumer = std::thread([&]() {
|
||||
while (true) {
|
||||
if (auto job = queue->pop(false)) {
|
||||
job();
|
||||
} else {
|
||||
if (doneProducing.load()) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
});
|
||||
std::thread nonBlockingPopBatchConsumer = std::thread([&]() {
|
||||
while (true) {
|
||||
utils::FixedCapacityVector<JobQueue::Job> jobs = queue->popBatch(2);
|
||||
if (!jobs.empty()) {
|
||||
for (auto& job : jobs) {
|
||||
job();
|
||||
}
|
||||
} else {
|
||||
if (doneProducing.load()) {
|
||||
break;
|
||||
}
|
||||
std::this_thread::yield();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Waiting for producers to complete pushing jobs
|
||||
for (auto& t : producers) {
|
||||
t.join();
|
||||
}
|
||||
doneProducing = true; // signal for non-blocking consumer
|
||||
queue->stop();
|
||||
|
||||
// Waiting for consumers to complete handling jobs
|
||||
blockingConsumer.join();
|
||||
nonBlockingConsumer.join();
|
||||
nonBlockingPopBatchConsumer.join();
|
||||
|
||||
EXPECT_EQ(NUM_THREADS * JOBS_PER_THREAD, v.load());
|
||||
}
|
||||
|
||||
|
||||
TEST(AmortizationWorker, Process) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobWorker::Ptr worker = AmortizationWorker::create(queue);
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
worker->process(2);
|
||||
EXPECT_EQ(2, v);
|
||||
|
||||
worker->process(1);
|
||||
EXPECT_EQ(3, v);
|
||||
|
||||
// No pending jobs, so it should be a no-op.
|
||||
worker->process(1);
|
||||
EXPECT_EQ(3, v);
|
||||
}
|
||||
|
||||
TEST(AmortizationWorker, ProcessAll) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobWorker::Ptr worker = AmortizationWorker::create(queue);
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
worker->process(-1);
|
||||
EXPECT_EQ(3, v);
|
||||
|
||||
// No pending jobs, so it should be a no-op.
|
||||
worker->process(1);
|
||||
EXPECT_EQ(3, v);
|
||||
}
|
||||
|
||||
TEST(AmortizationWorker, TerminateDrainsAllJobs) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobWorker::Ptr worker = AmortizationWorker::create(queue);
|
||||
int v = 0;
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
// `terminate` should drain all jobs
|
||||
worker->terminate();
|
||||
EXPECT_EQ(2, v);
|
||||
|
||||
// After terminate, pushing new jobs should not work.
|
||||
queue->push([&v]() { v++; });
|
||||
worker->process(1);
|
||||
EXPECT_EQ(2, v);
|
||||
}
|
||||
|
||||
|
||||
TEST(ThreadWorker, Process) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
JobWorker::Ptr worker = ThreadWorker::create(queue, {});
|
||||
std::atomic_int v = {0};
|
||||
|
||||
queue->push([&v]() { v++; });
|
||||
queue->push([&v]() { v++; });
|
||||
|
||||
// `terminate` should drain all jobs
|
||||
worker->terminate();
|
||||
EXPECT_EQ(2, v.load());
|
||||
|
||||
// After terminate, pushing new jobs should not work.
|
||||
queue->push([&v]() { v++; });
|
||||
worker->terminate();
|
||||
EXPECT_EQ(2, v.load());
|
||||
}
|
||||
|
||||
TEST(ThreadWorker, Callbacks) {
|
||||
JobQueue::Ptr queue = JobQueue::create();
|
||||
bool beginCalled = false;
|
||||
bool endCalled = false;
|
||||
|
||||
ThreadWorker::Config config = {
|
||||
.name = "TestThread",
|
||||
.priority = ThreadWorker::Priority::NORMAL,
|
||||
.onBegin = [&beginCalled]() { beginCalled = true; },
|
||||
.onEnd = [&endCalled]() { endCalled = true; }
|
||||
};
|
||||
JobWorker::Ptr worker = ThreadWorker::create(queue, std::move(config));
|
||||
worker->terminate();
|
||||
|
||||
EXPECT_TRUE(beginCalled);
|
||||
EXPECT_TRUE(endCalled);
|
||||
}
|
||||
@@ -371,7 +371,12 @@ public:
|
||||
//! Indicates whether an existing parameter is a sampler or not.
|
||||
bool isSampler(const char* UTILS_NONNULL name) const noexcept;
|
||||
|
||||
|
||||
/**
|
||||
* Returns a view of the material source (.mat which is a JSON-ish file) string,
|
||||
* if it has been set. Otherwise, it returns a view of an empty string.
|
||||
* The lifetime of the string_view is tied to the lifetime of the Material.
|
||||
*/
|
||||
std::string_view getSource() const noexcept;
|
||||
/**
|
||||
*
|
||||
* Gets the name of the transform field associated for the given sampler parameter.
|
||||
|
||||
@@ -146,6 +146,10 @@ bool Material::isSampler(const char* name) const noexcept {
|
||||
return downcast(this)->isSampler(name);
|
||||
}
|
||||
|
||||
std::string_view Material::getSource() const noexcept {
|
||||
return downcast(this)->getSource();
|
||||
}
|
||||
|
||||
const char* Material::getParameterTransformName(const char* samplerName) const noexcept {
|
||||
return downcast(this)->getParameterTransformName(samplerName);
|
||||
}
|
||||
|
||||
@@ -257,6 +257,8 @@ void MaterialDefinition::processMain() {
|
||||
bool const hasFog = !(variantFilterMask & UserVariantFilterMask(UserVariantFilterBit::FOG));
|
||||
|
||||
perViewLayoutIndex = ColorPassDescriptorSet::getIndex(isLit, isSSR, hasFog);
|
||||
|
||||
mMaterialParser->getSourceShader(&source);
|
||||
}
|
||||
|
||||
void MaterialDefinition::processBlendingMode() {
|
||||
|
||||
@@ -113,6 +113,7 @@ struct MaterialDefinition {
|
||||
|
||||
utils::CString name;
|
||||
uint64_t cacheId = 0;
|
||||
utils::CString source;
|
||||
|
||||
private:
|
||||
friend class MaterialCache;
|
||||
|
||||
@@ -35,6 +35,8 @@
|
||||
#include <backend/DriverEnums.h>
|
||||
#include <backend/Program.h>
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
#include <utils/compiler.h>
|
||||
#include <utils/CString.h>
|
||||
#include <utils/FixedCapacityVector.h>
|
||||
@@ -237,6 +239,40 @@ bool MaterialParser::getName(CString* cstring) const noexcept {
|
||||
return unflattener.read(cstring);
|
||||
}
|
||||
|
||||
bool MaterialParser::getSourceShader(CString* cstring) const noexcept {
|
||||
auto [start, end] = mImpl.mChunkContainer.getChunkRange(MaterialSource);
|
||||
// Source material is optional, treat it as a success.
|
||||
if (start == end) return true;
|
||||
|
||||
Unflattener unflattener(start, end);
|
||||
|
||||
// First get the compressed blob.
|
||||
const char* compressed;
|
||||
size_t compressedSize = 0;
|
||||
if (!unflattener.read(&compressed, &compressedSize)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get the bound and decompress it.
|
||||
const size_t decompressBound =
|
||||
ZSTD_getFrameContentSize(compressed, compressedSize);
|
||||
if (ZSTD_isError(decompressBound)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto dst_buffer = std::make_unique<char[]>(decompressBound);
|
||||
const size_t decompressed =
|
||||
ZSTD_decompress(
|
||||
dst_buffer.get(), decompressBound,
|
||||
compressed, compressedSize);
|
||||
if (ZSTD_isError(decompressed)) {
|
||||
return false;
|
||||
}
|
||||
*cstring = CString { dst_buffer.get(), decompressed };
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool MaterialParser::getCacheId(uint64_t* cacheId) const noexcept {
|
||||
auto [start, end] = mImpl.mChunkContainer.getChunkRange(MaterialCacheId);
|
||||
if (start == end) return false;
|
||||
|
||||
@@ -145,6 +145,8 @@ public:
|
||||
return getMaterialChunk().hasShader(model, variant, stage);
|
||||
}
|
||||
|
||||
bool getSourceShader(utils::CString* cstring) const noexcept;
|
||||
|
||||
filaflat::MaterialChunk const& getMaterialChunk() const noexcept {
|
||||
return mImpl.mMaterialChunk;
|
||||
}
|
||||
|
||||
@@ -252,6 +252,10 @@ public:
|
||||
return mUseUboBatching;
|
||||
}
|
||||
|
||||
std::string_view getSource() const noexcept {
|
||||
return mDefinition.source.c_str_safe();
|
||||
}
|
||||
|
||||
#if FILAMENT_ENABLE_MATDBG
|
||||
void applyPendingEdits() noexcept;
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ set_target_properties(test_material_parser PROPERTIES FOLDER Tests)
|
||||
add_executable(test_material
|
||||
filament_test_material.cpp
|
||||
${RESGEN_SOURCE})
|
||||
target_link_libraries(test_material PRIVATE filament gtest)
|
||||
target_link_libraries(test_material PRIVATE filamat filament gtest)
|
||||
target_compile_options(test_material PRIVATE ${COMPILER_FLAGS})
|
||||
target_include_directories(test_material PRIVATE ${RESOURCE_DIR})
|
||||
set_target_properties(test_material PROPERTIES FOLDER Tests)
|
||||
@@ -13,9 +13,10 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filamat/MaterialBuilder.h>
|
||||
|
||||
#include <filament/Engine.h>
|
||||
#include <filament/Material.h>
|
||||
|
||||
@@ -84,6 +85,53 @@ TEST(MaterialTransformName, QueryMultipleSamplersWithoutTransforms) {
|
||||
Engine::destroy(engine);
|
||||
}
|
||||
|
||||
TEST(Material, MaterialWithSourceMaterialSuccessfullyRetrieveSource) {
|
||||
// Need to set a specific backend to create a proper MaterialParser.
|
||||
Engine* engine = Engine::create(Engine::Backend::OPENGL);
|
||||
|
||||
std::string shaderCode(R"(
|
||||
void material(inout MaterialInputs material) {
|
||||
prepareMaterial(material);
|
||||
material.baseColor = vec4(1.);
|
||||
}
|
||||
)");
|
||||
filamat::MaterialBuilder builder;
|
||||
builder.init();
|
||||
builder.materialSource(shaderCode);
|
||||
filamat::Package result = builder.build(engine->getJobSystem());
|
||||
ASSERT_TRUE(result.isValid());
|
||||
|
||||
Material* material = Material::Builder()
|
||||
.package(result.getData(), result.getSize())
|
||||
.build(*engine);
|
||||
ASSERT_NE(material, nullptr);
|
||||
|
||||
EXPECT_EQ(material->getSource(), shaderCode);
|
||||
|
||||
engine->destroy(material);
|
||||
Engine::destroy(engine);
|
||||
}
|
||||
|
||||
|
||||
TEST(Material, MaterialWithoutSourceMaterialReturnsEmptySource) {
|
||||
// Need to set a specific backend to create a proper MaterialParser.
|
||||
Engine* engine = Engine::create(Engine::Backend::OPENGL);
|
||||
filamat::MaterialBuilder builder;
|
||||
builder.init();
|
||||
filamat::Package result = builder.build(engine->getJobSystem());
|
||||
ASSERT_TRUE(result.isValid());
|
||||
|
||||
Material* material = Material::Builder()
|
||||
.package(result.getData(), result.getSize())
|
||||
.build(*engine);
|
||||
ASSERT_NE(material, nullptr);
|
||||
|
||||
EXPECT_EQ(material->getSource(), "");
|
||||
|
||||
engine->destroy(material);
|
||||
Engine::destroy(engine);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
Pod::Spec.new do |spec|
|
||||
spec.name = "Filament"
|
||||
spec.version = "1.68.0"
|
||||
spec.version = "1.68.2"
|
||||
spec.license = { :type => "Apache 2.0", :file => "LICENSE" }
|
||||
spec.homepage = "https://google.github.io/filament"
|
||||
spec.authors = "Google LLC."
|
||||
spec.summary = "Filament is a real-time physically based rendering engine for Android, iOS, Windows, Linux, macOS, and WASM/WebGL."
|
||||
spec.platform = :ios, "11.0"
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.68.0/filament-v1.68.0-ios.tgz" }
|
||||
spec.source = { :http => "https://github.com/google/filament/releases/download/v1.68.2/filament-v1.68.2-ios.tgz" }
|
||||
|
||||
# Fix linking error with Xcode 12; we do not yet support the simulator on Apple silicon.
|
||||
spec.pod_target_xcconfig = {
|
||||
|
||||
@@ -101,6 +101,8 @@ enum UTILS_PUBLIC ChunkType : uint64_t {
|
||||
DictionaryMetalLibrary = charTo64bitNum("DIC_MLIB"),
|
||||
|
||||
MaterialCrc32 = charTo64bitNum("MAT_CRC "),
|
||||
|
||||
MaterialSource = charTo64bitNum("MAT_SRC "),
|
||||
};
|
||||
|
||||
} // namespace filamat
|
||||
|
||||
@@ -16,6 +16,7 @@ set(HDRS
|
||||
set(COMMON_PRIVATE_HDRS
|
||||
src/eiff/Chunk.h
|
||||
src/eiff/ChunkContainer.h
|
||||
src/eiff/CompressedStringChunk.h
|
||||
src/eiff/DictionaryTextChunk.h
|
||||
src/eiff/Flattener.h
|
||||
src/eiff/LineDictionary.h
|
||||
@@ -28,6 +29,7 @@ set(COMMON_PRIVATE_HDRS
|
||||
set(COMMON_SRCS
|
||||
src/eiff/Chunk.cpp
|
||||
src/eiff/ChunkContainer.cpp
|
||||
src/eiff/CompressedStringChunk.cpp
|
||||
src/eiff/DictionaryTextChunk.cpp
|
||||
src/eiff/LineDictionary.cpp
|
||||
src/eiff/MaterialTextChunk.cpp
|
||||
@@ -81,7 +83,7 @@ include_directories(${CMAKE_BINARY_DIR})
|
||||
add_library(${TARGET} STATIC ${HDRS} ${PRIVATE_HDRS} ${SRCS})
|
||||
target_include_directories(${TARGET} PUBLIC ${PUBLIC_HDR_DIR})
|
||||
set_target_properties(${TARGET} PROPERTIES FOLDER Libs)
|
||||
target_link_libraries(${TARGET} backend_headers shaders filabridge utils smol-v)
|
||||
target_link_libraries(${TARGET} backend_headers shaders filabridge utils smol-v zstd)
|
||||
|
||||
if (FILAMENT_SUPPORTS_WEBGPU)
|
||||
target_link_libraries(${TARGET} libtint)
|
||||
@@ -133,6 +135,7 @@ set(FILAMAT_DEPS
|
||||
spirv-cross-core
|
||||
spirv-cross-glsl
|
||||
spirv-cross-msl
|
||||
zstd
|
||||
)
|
||||
|
||||
set(FILAMAT_COMBINED_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/libfilamat_combined.a")
|
||||
|
||||
@@ -660,6 +660,12 @@ public:
|
||||
*/
|
||||
MaterialBuilder& useDefaultDepthVariant() noexcept;
|
||||
|
||||
/**
|
||||
* Sets the source ASCII material (aka .mat file).
|
||||
* The provided `source` string_view must remain valid until MaterialBuilder::build() is called.
|
||||
*/
|
||||
MaterialBuilder& materialSource(std::string_view source) noexcept;
|
||||
|
||||
/**
|
||||
* Build the material. If you are using the Filament engine with this library, you should use
|
||||
* the job system provided by Engine.
|
||||
@@ -905,6 +911,7 @@ private:
|
||||
|
||||
ShaderCode mMaterialFragmentCode;
|
||||
ShaderCode mMaterialVertexCode;
|
||||
std::string_view mMaterialSource;
|
||||
|
||||
PropertyList mProperties;
|
||||
ParameterList mParameters;
|
||||
|
||||
@@ -31,6 +31,7 @@
|
||||
|
||||
#include "eiff/BlobDictionary.h"
|
||||
#include "eiff/ChunkContainer.h"
|
||||
#include "eiff/CompressedStringChunk.h"
|
||||
#include "eiff/DictionarySpirvChunk.h"
|
||||
#include "eiff/DictionaryTextChunk.h"
|
||||
#include "eiff/LineDictionary.h"
|
||||
@@ -1251,6 +1252,11 @@ MaterialBuilder& MaterialBuilder::useLegacyMorphing() noexcept {
|
||||
return *this;
|
||||
}
|
||||
|
||||
MaterialBuilder& MaterialBuilder::materialSource(std::string_view source) noexcept {
|
||||
mMaterialSource = source;
|
||||
return *this;
|
||||
}
|
||||
|
||||
Package MaterialBuilder::build(JobSystem& jobSystem) {
|
||||
if (materialBuilderClients == 0) {
|
||||
slog.e << "Error: MaterialBuilder::init() must be called before build()." << io::endl;
|
||||
@@ -1701,6 +1707,11 @@ void MaterialBuilder::writeCommonChunks(ChunkContainer& container, MaterialInfo&
|
||||
hasher({ frag.data(), frag.size() })));
|
||||
|
||||
container.emplace<uint64_t>(MaterialCacheId, materialId);
|
||||
if (!mMaterialSource.empty()) {
|
||||
container.push<CompressedStringChunk>(
|
||||
MaterialSource, mMaterialSource,
|
||||
CompressedStringChunk::CompressionLevel::MAX);
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialBuilder::writeSurfaceChunks(ChunkContainer& container) const noexcept {
|
||||
|
||||
53
libs/filamat/src/eiff/CompressedStringChunk.cpp
Normal file
53
libs/filamat/src/eiff/CompressedStringChunk.cpp
Normal file
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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 "CompressedStringChunk.h"
|
||||
|
||||
#include <zstd.h>
|
||||
|
||||
namespace filamat {
|
||||
|
||||
namespace {
|
||||
int toZstdCompressionLevel(CompressedStringChunk::CompressionLevel compressionLevel) {
|
||||
switch (compressionLevel) {
|
||||
case CompressedStringChunk::CompressionLevel::MIN:
|
||||
return ZSTD_minCLevel();
|
||||
case CompressedStringChunk::CompressionLevel::MAX:
|
||||
return ZSTD_maxCLevel();
|
||||
case CompressedStringChunk::CompressionLevel::DEFAULT:
|
||||
return ZSTD_defaultCLevel();
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
void CompressedStringChunk::flatten(filamat::Flattener& f) {
|
||||
const size_t bufferBound = ZSTD_compressBound(mString.size());
|
||||
std::vector<std::uint8_t> compressed(bufferBound);
|
||||
|
||||
const size_t compressedSize = ZSTD_compress(
|
||||
compressed.data(), compressed.size(),
|
||||
mString.data(), mString.size(),
|
||||
toZstdCompressionLevel(mCompressionLevel));
|
||||
|
||||
if (ZSTD_isError(compressedSize)) {
|
||||
utils::slog.e << "Error compressing the input string." << utils::io::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
f.writeBlob((const char*) compressed.data(), compressedSize);
|
||||
}
|
||||
|
||||
} // namespace filamat
|
||||
46
libs/filamat/src/eiff/CompressedStringChunk.h
Normal file
46
libs/filamat/src/eiff/CompressedStringChunk.h
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (C) 2025 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.
|
||||
*/
|
||||
|
||||
#ifndef TNT_COMPRESSEDSTRINGCHUNK_H
|
||||
#define TNT_COMPRESSEDSTRINGCHUNK_H
|
||||
|
||||
#include "Chunk.h"
|
||||
#include "Flattener.h"
|
||||
|
||||
#include <utils/CString.h>
|
||||
|
||||
namespace filamat {
|
||||
|
||||
class CompressedStringChunk final : public Chunk {
|
||||
public:
|
||||
enum class CompressionLevel { MIN, MAX, DEFAULT };
|
||||
CompressedStringChunk(
|
||||
ChunkType type, std::string_view string, CompressionLevel compressionLevel)
|
||||
: Chunk(type),
|
||||
mString(utils::CString(string.data(), string.size())),
|
||||
mCompressionLevel(compressionLevel) {}
|
||||
~CompressedStringChunk() override = default;
|
||||
|
||||
private:
|
||||
void flatten(Flattener& f) override;
|
||||
utils::CString mString;
|
||||
CompressionLevel mCompressionLevel;
|
||||
};
|
||||
|
||||
} // namespace filamat
|
||||
|
||||
|
||||
#endif // TNT_COMPRESSEDSTRINGCHUNK_H
|
||||
@@ -964,6 +964,20 @@ TEST_F(MaterialCompiler, FeatureLevel0Ess3CallFails) {
|
||||
EXPECT_FALSE(result.isValid());
|
||||
}
|
||||
|
||||
TEST_F(MaterialCompiler, EmbedMaterialSourceSucceeds) {
|
||||
std::string shaderCode(R"(
|
||||
void material(inout MaterialInputs material) {
|
||||
prepareMaterial(material);
|
||||
material.baseColor = vec4(1.);
|
||||
}
|
||||
)");
|
||||
filamat::MaterialBuilder builder;
|
||||
builder.materialSource(shaderCode);
|
||||
|
||||
filamat::Package result = builder.build(*jobSystem);
|
||||
EXPECT_TRUE(result.isValid());
|
||||
}
|
||||
|
||||
#if FILAMENT_SUPPORTS_WEBGPU
|
||||
TEST_F(MaterialCompiler, WgslConversionBakedColor) {
|
||||
std::string bakedColorCodeFrag(R"(
|
||||
|
||||
@@ -157,6 +157,7 @@ public:
|
||||
|
||||
bool getInsertLineDirectives() const noexcept { return mInsertLineDirectives; }
|
||||
bool getInsertLineDirectiveChecks() const noexcept { return mInsertLineDirectiveChecks; }
|
||||
bool getIncludeSourceMaterial() const noexcept { return mIncludeSourceMaterial; }
|
||||
|
||||
protected:
|
||||
bool mDebug = false;
|
||||
@@ -179,6 +180,7 @@ protected:
|
||||
bool mIncludeEssl1 = true;
|
||||
bool mInsertLineDirectives = true;
|
||||
bool mInsertLineDirectiveChecks = true;
|
||||
bool mIncludeSourceMaterial = false;
|
||||
};
|
||||
|
||||
} // namespace matp
|
||||
|
||||
@@ -70,6 +70,7 @@ static const option OPTIONS[] = {
|
||||
{ "workarounds", required_argument, nullptr, 'W' },
|
||||
{ "no-insert-line-directives", no_argument, nullptr, 'n' },
|
||||
{ "no-insert-line-directive-check", no_argument, nullptr, 'N' },
|
||||
{ "include-source-mat", no_argument, nullptr, 'm' },
|
||||
{ nullptr, 0, nullptr, 0 } // termination of the option list
|
||||
};
|
||||
|
||||
@@ -163,6 +164,8 @@ static void usage(char* name) {
|
||||
" #if defined(GL_GOOGLE_cpp_style_line_directive)\n"
|
||||
" Some drivers may complain about the use of cpp style #line directives\n"
|
||||
" if they don't support it.\n\n"
|
||||
" --include-source-mat\n"
|
||||
" Embeds the source ASCII material definition if set.\n"
|
||||
|
||||
"Internal use and debugging only:\n"
|
||||
" --optimize-none, -g, -O0\n"
|
||||
@@ -406,6 +409,9 @@ bool CommandlineConfig::parse() {
|
||||
case 'N':
|
||||
mInsertLineDirectiveChecks = false;
|
||||
break;
|
||||
case 'm':
|
||||
mIncludeSourceMaterial = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -98,6 +98,10 @@ bool MaterialCompiler::run(const matp::Config& config) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.getIncludeSourceMaterial()) {
|
||||
builder.materialSource(buffer.get());
|
||||
}
|
||||
|
||||
// If we're reflecting parameters, the MaterialParser will have handled it inside of parse().
|
||||
// We should return here to avoid actually building a material.
|
||||
if (config.getReflectionTarget() != matp::Config::Metadata::NONE) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "filament",
|
||||
"version": "1.68.0",
|
||||
"version": "1.68.2",
|
||||
"description": "Real-time physically based rendering engine",
|
||||
"main": "filament.js",
|
||||
"module": "filament.js",
|
||||
|
||||
Reference in New Issue
Block a user