Compare commits

...

5 Commits

Author SHA1 Message Date
Benjamin Doherty
474a4f3fcd Bump version to 1.68.2 2025-12-10 11:05:05 -08:00
Benjamin Doherty
d4efef9a9b Release Filament 1.68.1 2025-12-10 11:04:56 -08:00
yein
cdffc9eaa0 Allow embedding material source in cmat (#9484)
* Allow embedding material source in cmat
* Compress material string
2025-12-10 10:31:51 -08:00
Sungun Park
a162a65dce Add JobQueue (#9480)
JobQueue is a thread-safe producer-consumer queue, which will be used
for asynchronous operations.
2025-12-10 18:00:58 +00:00
Ben Doherty
369bab4744 Implement setPresentationTime for Metal (#9470) 2025-12-09 14:54:06 -08:00
31 changed files with 1121 additions and 22 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View 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

View 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

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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;

View 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);
}

View File

@@ -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.

View File

@@ -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);
}

View File

@@ -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() {

View File

@@ -113,6 +113,7 @@ struct MaterialDefinition {
utils::CString name;
uint64_t cacheId = 0;
utils::CString source;
private:
friend class MaterialCache;

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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)

View File

@@ -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();

View File

@@ -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 = {

View File

@@ -101,6 +101,8 @@ enum UTILS_PUBLIC ChunkType : uint64_t {
DictionaryMetalLibrary = charTo64bitNum("DIC_MLIB"),
MaterialCrc32 = charTo64bitNum("MAT_CRC "),
MaterialSource = charTo64bitNum("MAT_SRC "),
};
} // namespace filamat

View File

@@ -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")

View File

@@ -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;

View File

@@ -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 {

View 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

View 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

View File

@@ -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"(

View File

@@ -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

View File

@@ -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;
}
}

View File

@@ -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) {

View File

@@ -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",