לדלג לתוכן

תורים שעונים ובעלות בחומרה

נניח שמילאנו buffer והעברנו אותו להתקן. הפונקציה חזרה, ולכן אנחנו ממלאים אותו בנתונים של הפעולה הבאה. לפעמים הכול עובד ולפעמים מופיעה תמונה משובשת או קפיצה בצליל. כבר פגשנו את הבאג הזה בI/O אסינכרוני; עכשיו נראה איך הוא מופיע גם כשהצרכן הוא רכיב חומרה.

המילה ״הושלם״ צריכה כתובת. הפעולה יכולה להסתיים מבחינת הCPU שמילא תור, ועדיין לא להסתיים מבחינת הGPU שקורא את הbuffer. גם העתקה לתור אודיו אינה אומרת שהצליל כבר הגיע לרמקול. לכל תחום יש ציר זמן וחוזה משלו.

מסמנים נקודה בציר הזמן של הGPU

בתור של Direct3D 12 אפשר להציב signal לfence אחרי העבודה שמשתמשת במשאב. רק כאשר ערך ההשלמה הגיע לסימון המתאים מותר למחזר את אותו חלק של הזיכרון בהתאם לחוזה שהגדרנו. הCPU יכול לרוץ קדימה כל עוד יש מקום פנוי; כשהטבעת מלאה צריך להמתין או להפעיל backpressure. אפשר לעקוב אחר דוגמת upload ring בתיעוד ניהול משאבים באמצעות fences.

נניח שיש לנו שלושה slots, והעבודה האחרונה שקראה מכל אחד מסומנת 17, 18 ו19. אם התור השלים 18, שני הslots הראשונים ניתנים למחזור והשלישי עדיין בשימוש. הסימון חייב להשתייך לאותו רצף עבודה; מספר גדול מfence אחר אינו ראיה לכלום.

המודל הבא מאפשר לבדוק את ההיגיון בלי GPU. הוא מניח תור יחיד וערכים עולים ללא wrap במשך חיי הניסוי. אין כאן סימולציה של cache של התקן או של resource barriers.

#include <array>
#include <cassert>
#include <cstdint>
#include <optional>
#include <stdexcept>

class Slots {
    struct Slot { bool busy = false; std::uint64_t fence = 0; };
    std::array<Slot, 3> slots{};
    std::uint64_t next = 1, completed = 0;
public:
    std::optional<std::uint64_t> submit(std::size_t index) {
        auto& slot = slots.at(index);
        if (slot.busy && slot.fence > completed) return std::nullopt;
        if (next == UINT64_MAX) throw std::overflow_error("fence exhausted");
        slot = {true, next++};
        return slot.fence;
    }
    void observe(std::uint64_t value) {
        if (value < completed || value >= next) throw std::logic_error("bad observation");
        completed = value;
    }
};
int main() {
    Slots ring;
    assert(ring.submit(0) == 1);
    assert(ring.submit(1) == 2);
    assert(ring.submit(2) == 3);
    assert(!ring.submit(0));
    ring.observe(2);
    assert(ring.submit(0) == 4);
    assert(!ring.submit(2));
    ring.observe(3);
    assert(ring.submit(2) == 5);
}

הבדיקה המעניינת היא הניסיון למחזר את slot 2 אחרי שהשלמנו רק 2. הוא נכשל גם אם הCPU פנוי וגם אם עברו כמה שניות. הבעלות תלויה בראיה מהתור, ולא בתחושת זמן.

כמה זמן נמצא בתוך buffer של אודיו

נפתח עכשיו endpoint אמיתי ונשאל מהו פורמט המיקס שלו. הקוד אינו משמיע צליל ואינו מקליט. הוא משתמש בCOM בדיוק כמו שלמדנו בפרק 9.

#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <mmdeviceapi.h>
#include <audioclient.h>
#include <wrl/client.h>
#include <cstdio>
#include <memory>
using Microsoft::WRL::ComPtr;
struct FreeFormat { void operator()(WAVEFORMATEX* p) const { CoTaskMemFree(p); } };
int inspect_audio() {
    ComPtr<IMMDeviceEnumerator> enumerator;
    HRESULT hr = CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr,
        CLSCTX_INPROC_SERVER, IID_PPV_ARGS(enumerator.GetAddressOf()));
    if (FAILED(hr)) return 2;
    ComPtr<IMMDevice> endpoint;
    hr = enumerator->GetDefaultAudioEndpoint(eRender, eConsole, endpoint.GetAddressOf());
    if (FAILED(hr)) return 3;
    ComPtr<IAudioClient> client;
    hr = endpoint->Activate(__uuidof(IAudioClient), CLSCTX_INPROC_SERVER, nullptr,
        reinterpret_cast<void**>(client.GetAddressOf()));
    if (FAILED(hr)) return 4;
    WAVEFORMATEX* raw = nullptr;
    hr = client->GetMixFormat(&raw);
    if (FAILED(hr)) return 5;
    std::unique_ptr<WAVEFORMATEX, FreeFormat> format(raw);
    if (format->nSamplesPerSec == 0 || format->nBlockAlign == 0) return 6;
    hr = client->Initialize(AUDCLNT_SHAREMODE_SHARED, 0, 1000000, 0,
                            format.get(), nullptr);
    if (FAILED(hr)) return 7;
    UINT32 frames = 0, padding = 0;
    if (FAILED(client->GetBufferSize(&frames))) return 8;
    if (FAILED(client->GetCurrentPadding(&padding))) return 9;
    std::printf("rate=%lu channels=%u frame_bytes=%u capacity=%u padding=%u duration_ms=%.3f\n",
        format->nSamplesPerSec, format->nChannels, format->nBlockAlign, frames, padding,
        1000.0 * frames / format->nSamplesPerSec);
    return 0;
}
int main() {
    HRESULT hr = CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    if (FAILED(hr)) return 1;
    int result = inspect_audio();
    CoUninitialize();
    return result;
}

הבנייה משתמשת בWindows SDK ובפקודה הבאה.

cl /nologo /std:c++20 /EHsc /W4 audio_info.cpp ole32.lib uuid.lib

הפורמט שחזר שייך לshared audio engine. הוא אינו בהכרח הפורמט שבו החומרה עצמה עובדת בexclusive mode, והזיכרון שלו משתחרר באמצעות CoTaskMemFree. הפרטים האלה מוגדרים בחוזה GetMixFormat.

המספר שהדפסנו הוא משך המסגרות בקיבולת הbuffer, לא מדידת latency מקצה לקצה. לפני Start אין כאן זרם פעיל, ולכן גם padding אינו מדידת התקדמות של ניגון. כדי למדוד את הדרך עד להתקן צריך להגדיר איזו נקודה משווים לאיזה clock, ולכלול תורים והמרות בדרך.

מה משותף לUSB ולצליל שנקטע

התקן USB יכול להיעלם בזמן שיש בקשות ממתינות. המשך חיי הbuffer נקבע לפי חוזה סיום הבקשה, גם אם המשתמש כבר ניתק את הכבל. יש להבדיל בין סוגי העברה: בקשה עם retry ברמת transport אינה מבטיחה אותו חוזה זמן כמו זרם isochronous.

העבודה עם HID מוסיפה report format. הגודל והמשמעות נקבעים לפי תיאור ההתקן והAPI הרלוונטי, ולא לפי struct נוח שהמצאנו. תוודאו גם אם הreport ID תופס byte בממשק שבו אתם משתמשים. את הכללים האלה בודקים מול ההתקן והמסלול הנבחרים לפני ששולחים buffer.

המעבדה המעמיקה בוחרת תחום אחד: GPU, אודיו או התקן בדיקה. בכל בחירה מציירים את הציר של הproducer ושל הconsumer, מסמנים היכן מותר למחזר זיכרון ומכניסים כשל באמצע. השאלה שמחברת את התחומים נשארת אותה שאלה: איזו ראיה מאפשרת לנו לגעת שוב במשאב.