Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 7a9735b3 authored by Lloyd Pique's avatar Lloyd Pique
Browse files

end2end: Generate Hwc3 vsync events [5/N]

When SurfaceFlinger asks for vsync to be enabled, we need to provide
periodic event callbacks through the callback interface.

This change adds a simple generator that can handle any number of
displays, with arbitrary timing. The generator is owned by the service
implementation, and forwards generated events to the client
implementation for it to send to SurfaceFlinger.

Flag: TEST_ONLY
Bug: 372735083
Test: atest surfaceflinger_end2end_tests
Change-Id: I56d7f832609a34d4dc02a4773b8b1a4a0826650d
parent e226cba6
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -34,12 +34,18 @@ cc_test {
        "main.cpp",
        "test_framework/core/EdidBuilder.cpp",
        "test_framework/core/TestService.cpp",
        "test_framework/hwc3/DisplayVSyncEventService.cpp",
        "test_framework/hwc3/FakeComposer.cpp",
        "test_framework/hwc3/Hwc3Controller.cpp",
        "test_framework/hwc3/MultiDisplayRefreshEventGenerator.cpp",
        "test_framework/hwc3/SingleDisplayRefreshEventGenerator.cpp",
        "test_framework/hwc3/SingleDisplayRefreshSchedule.cpp",
        "test_framework/hwc3/TimeKeeperThread.cpp",
        "test_framework/surfaceflinger/SFController.cpp",

        // Internal tests
        "tests/internal/AsyncFunction_test.cpp",
        "tests/internal/DisplayRefresh_test.cpp",
        "tests/internal/EdidBuilder_test.cpp",

        // SurfaceFlinger tests
+51 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.
 */

#pragma once

#include <chrono>
#include <string>

#include <fmt/chrono.h>  // NOLINT(misc-include-cleaner)
#include <fmt/format.h>

namespace android::surfaceflinger::tests::end2end::test_framework::core {

// Represents a span of time between two absolute points in time.
// The interval is half-open, including the beginning point, but not the end point.
// Note: "interval" is used to be consistent with the mathematical definition of the term.
// NOLINTBEGIN(misc-non-private-member-variables-in-classes)
struct TimeInterval final {
    using TimePoint = std::chrono::steady_clock::time_point;

    // The half-open interval [begin, end), where the end time is not included.
    TimePoint begin;
    TimePoint end;

    [[nodiscard]] auto empty() const -> bool { return begin == end; }
    friend auto operator==(const TimeInterval&, const TimeInterval&) -> bool = default;
};
// NOLINTEND(misc-non-private-member-variables-in-classes)

inline auto toString(const TimeInterval& value) -> std::string {
    return fmt::format("[{}, {})", value.begin.time_since_epoch(), value.end.time_since_epoch());
}

inline auto format_as(const TimeInterval& event) -> std::string {
    return toString(event);
}

}  // namespace android::surfaceflinger::tests::end2end::test_framework::core
+106 −0
Original line number Diff line number Diff line
/*
 * Copyright 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 <memory>
#include <string>
#include <utility>
#include <vector>

#include <android-base/expected.h>
#include <android-base/logging.h>
#include <ftl/ignore.h>

#include "test_framework/core/GuardedSharedState.h"
#include "test_framework/hwc3/DisplayVSyncEventService.h"
#include "test_framework/hwc3/MultiDisplayRefreshEventGenerator.h"
#include "test_framework/hwc3/SingleDisplayRefreshSchedule.h"
#include "test_framework/hwc3/TimeKeeperThread.h"
#include "test_framework/hwc3/events/VSync.h"

namespace android::surfaceflinger::tests::end2end::test_framework::hwc3 {

struct DisplayVSyncEventService::Passkey final {};

auto DisplayVSyncEventService::make()
        -> base::expected<std::shared_ptr<DisplayVSyncEventService>, std::string> {
    using namespace std::string_literals;

    auto service = std::make_unique<DisplayVSyncEventService>(Passkey{});
    if (service == nullptr) {
        return base::unexpected("Failed to construct the DisplayVSyncEventService"s);
    }
    if (auto result = service->init(); !result) {
        return base::unexpected("Failed to init the DisplayVSyncEventService: "s + result.error());
    }
    return service;
}

DisplayVSyncEventService::DisplayVSyncEventService(Passkey passkey) {
    ftl::ignore(passkey);
}

auto DisplayVSyncEventService::init() -> base::expected<void, std::string> {
    auto timekeeperMake = TimeKeeperThread::make();
    if (!timekeeperMake) {
        return base::unexpected(std::move(timekeeperMake.error()));
    }

    mTimeKeeper = *std::move(timekeeperMake);
    mTimeKeeper->editCallbacks().onWakeup.set(
            [this](TimeInterval sleeping) -> TimePoint { return onWakeup(sleeping); })();
    return {};
}

auto DisplayVSyncEventService::editCallbacks() -> Callbacks& {
    return mCallbacks;
}

void DisplayVSyncEventService::addDisplay(DisplayId displayId,
                                          SingleDisplayRefreshSchedule schedule) {
    mGenerator.withExclusiveLock([displayId, schedule](auto& generator) {
        LOG(VERBOSE) << "addDisplay: " << displayId << " schedule: " << toString(schedule);
        generator.addDisplay(displayId, schedule);
    });

    mTimeKeeper->wakeNow();
}

void DisplayVSyncEventService::removeDisplay(DisplayId displayId) {
    mGenerator.withExclusiveLock([displayId](auto& generator) {
        LOG(VERBOSE) << "remove " << displayId;
        generator.removeDisplay(displayId);
    });
}

auto DisplayVSyncEventService::onWakeup(TimeInterval elapsed) -> TimePoint {
    // Obtain all the events to emit.
    auto result = mGenerator.withExclusiveLock(
            [elapsed](auto& generator) { return generator.generateEventsFor(elapsed); });

    // Emit them via the connector interface.
    LOG(VERBOSE) << "emitting " << result.events.size() << " events";
    for (const auto& event : result.events) {
        LOG(VERBOSE) << "displayId: " << event.displayId
                     << " timestamp: " << event.expectedAt.time_since_epoch()
                     << " period: " << event.expectedPeriod;
        mCallbacks.onVSync(event);
    }

    LOG(VERBOSE) << "next wakeup " << result.nextRefreshAt.time_since_epoch();
    return result.nextRefreshAt;
}

}  // namespace android::surfaceflinger::tests::end2end::test_framework::hwc3
+74 −0
Original line number Diff line number Diff line
/*
 * Copyright 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.
 */

#pragma once

#include <chrono>
#include <memory>
#include <string>

#include <android-base/expected.h>

#include "test_framework/core/DisplayConfiguration.h"
#include "test_framework/core/GuardedSharedState.h"
#include "test_framework/core/TimeInterval.h"
#include "test_framework/hwc3/MultiDisplayRefreshEventGenerator.h"
#include "test_framework/hwc3/SingleDisplayRefreshSchedule.h"
#include "test_framework/hwc3/events/VSync.h"

namespace android::surfaceflinger::tests::end2end::test_framework::hwc3 {

class TimeKeeperThread;

class DisplayVSyncEventService final {
    struct Passkey;  // Uses the passkey idiom to restrict construction.

  public:
    struct Callbacks final {
        events::VSync::AsyncConnector onVSync;
    };

    using DisplayId = core::DisplayConfiguration::Id;

    [[nodiscard]] static auto make()
            -> base::expected<std::shared_ptr<DisplayVSyncEventService>, std::string>;

    explicit DisplayVSyncEventService(Passkey passkey);

    [[nodiscard]] auto editCallbacks() -> Callbacks&;

    void addDisplay(DisplayId displayId, SingleDisplayRefreshSchedule schedule);

    void removeDisplay(DisplayId displayId);

  private:
    using TimePoint = std::chrono::steady_clock::time_point;
    using Duration = std::chrono::steady_clock::duration;
    using TimeInterval = core::TimeInterval;

    auto init() -> base::expected<void, std::string>;
    auto onWakeup(TimeInterval elapsed) -> TimePoint;

    core::GuardedSharedState<MultiDisplayRefreshEventGenerator> mGenerator;

    // No mutex needed.
    Callbacks mCallbacks;

    // Note: The time keeper thread should be destroyed first, so is declared last.
    std::unique_ptr<TimeKeeperThread> mTimeKeeper;
};

}  // namespace android::surfaceflinger::tests::end2end::test_framework::hwc3
+79 −1
Original line number Diff line number Diff line
@@ -23,6 +23,7 @@
#include <initializer_list>
#include <memory>
#include <optional>
#include <ratio>
#include <source_location>
#include <string>
#include <string_view>
@@ -58,10 +59,13 @@
#include "test_framework/core/DisplayConfiguration.h"
#include "test_framework/core/EdidBuilder.h"
#include "test_framework/core/GuardedSharedState.h"
#include "test_framework/hwc3/DisplayVSyncEventService.h"
#include "test_framework/hwc3/FakeComposer.h"
#include "test_framework/hwc3/FakeDisplayConfiguration.h"
#include "test_framework/hwc3/SingleDisplayRefreshSchedule.h"
#include "test_framework/hwc3/delegators/Composer.h"
#include "test_framework/hwc3/delegators/ComposerClient.h"
#include "test_framework/hwc3/events/VSync.h"

namespace android::surfaceflinger::tests::end2end::test_framework::hwc3 {

@@ -248,6 +252,16 @@ struct ClientImpl final : public DefaultComposerClientImpl {
        });
    }

    void sendVSync(events::VSync event) {
        mState.withSharedLock([event](const auto& state) {
            state.composerCallbacks->onVsync(
                    event.displayId, event.expectedAt.time_since_epoch().count(),
                    std::chrono::duration_cast<std::chrono::duration<int32_t, std::nano>>(
                            event.expectedPeriod)
                            .count());
        });
    }

    // begin IComposerClient overrides
    // NOLINTBEGIN(bugprone-easily-swappable-parameters)

@@ -569,6 +583,19 @@ struct ClientImpl final : public DefaultComposerClientImpl {
};

struct FakeComposer::ComposerImpl final : public DefaultComposerImpl {
    auto init() -> base::expected<void, std::string> {
        auto displayVsyncEventServiceResult = DisplayVSyncEventService::make();
        if (!displayVsyncEventServiceResult) {
            return base::unexpected(std::move(displayVsyncEventServiceResult).error());
        }

        mDisplayVSyncEventService = *std::move(displayVsyncEventServiceResult);
        mDisplayVSyncEventService->editCallbacks().onVSync.set(
                [this](auto event) { onVsyncEventGenerated(event); })();

        return {};
    }

    auto getComposer() -> std::shared_ptr<IComposer> { return ref<ComposerImpl>(); }

    void addDisplay(const core::DisplayConfiguration& display) {
@@ -603,6 +630,8 @@ struct FakeComposer::ComposerImpl final : public DefaultComposerImpl {
    }

    void removeDisplay(core::DisplayConfiguration::Id displayId) {
        mDisplayVSyncEventService->removeDisplay(displayId);

        auto client = mState.withExclusiveLock([displayId](auto& state) {
            if (state.initialDisplayId == displayId) {
                state.initialDisplayId.reset();
@@ -687,6 +716,51 @@ struct FakeComposer::ComposerImpl final : public DefaultComposerImpl {
        });
    }

    void onVsyncEventGenerated(events::VSync event) {
        if (auto client = mState.withSharedLock([](const auto& state) { return state.client; })) {
            client->sendVSync(event);
        }
    }

    void onVsyncEnabledChanged(int64_t displayId, bool enable) const {
        LOG(VERBOSE) << __func__;
        mState.withSharedLock([this, displayId, enable](const auto& state) {
            LOG(INFO) << "displayId: " << displayId << " enable: " << enable;
            if (!enable) {
                mDisplayVSyncEventService->removeDisplay(displayId);
                return;
            }

            auto found = state.configuredDisplays.find(displayId);
            if (found == state.configuredDisplays.end()) {
                LOG(WARNING) << "No display " << displayId << " to configure vsync generation.";
                return;
            }
            const auto& display = found->second;
            auto foundConfig = std::ranges::find_if(
                    display.displayConfigs, [configId = display.activeConfig](const auto& config) {
                        return config.configId == configId;
                    });
            if (foundConfig == display.displayConfigs.end()) {
                LOG(WARNING) << "No config " << display.activeConfig << " for display " << displayId
                             << " to configure vsync generation.";
                return;
            }
            const auto& config = *foundConfig;

            using TimePoint = std::chrono::steady_clock::time_point;
            using Duration = std::chrono::steady_clock::duration;

            LOG(WARNING) << "Assuming a base of zero for vsync timing!";
            const TimePoint base{Duration(0)};
            const auto period = Duration(config.vsyncPeriod);
            mDisplayVSyncEventService->addDisplay(
                    displayId, SingleDisplayRefreshSchedule{.base = base, .period = period});
        });
    }

    std::shared_ptr<DisplayVSyncEventService> mDisplayVSyncEventService;

    struct State {
        std::shared_ptr<ClientImpl> client;
        std::optional<int64_t> initialDisplayId;
@@ -725,7 +799,11 @@ auto FakeComposer::init() -> base::expected<void, std::string> {

    auto impl = ndk::SharedRefBase::make<ComposerImpl>();
    if (!impl) {
        return base::unexpected("Failed to construct the Hwc3ComposerImpl instance."s);
        return base::unexpected("Failed to construct the ComposerImpl instance."s);
    }

    if (auto result = impl->init(); !result) {
        return base::unexpected("Failed to init the ComposerImpl instance: "s + result.error());
    }

    mImpl = std::move(impl);
Loading