Title: [291369] trunk/Source
Revision
291369
Author
mmaxfi...@apple.com
Date
2022-03-16 14:49:48 -0700 (Wed, 16 Mar 2022)

Log Message

[WebGPU] Implement first draft of buffer mapping according to the spec
https://bugs.webkit.org/show_bug.cgi?id=237870

Reviewed by Kimmo Kinnunen.

Source/WebGPU:

Implement the various GPUBuffer methods, according to the algorithms in the spec.
There are a few things which the spec lists which we can't do yet (like reporting
validation errors), so those things are left with FIXMEs. Every step is listed with
links to the spec where appropriate, and with quotes to the spec describing what
is being implemented.

* WebGPU.xcodeproj/project.pbxproj:
* WebGPU/Buffer.h:
(WebGPU::Buffer::create):
(WebGPU::Buffer::size const):
(WebGPU::Buffer::usage const):
* WebGPU/Buffer.mm:
(WebGPU::validateDescriptor):
(WebGPU::validateCreateBuffer):
(WebGPU::storageMode):
(WebGPU::Device::createBuffer):
(WebGPU::Buffer::Buffer):
(WebGPU::Buffer::destroy):
(WebGPU::Buffer::getConstMappedRange):
(WebGPU::Buffer::validateGetMappedRange const):
(WebGPU::Buffer::getMappedRange):
(WebGPU::Buffer::validateMapAsync const):
(WebGPU::Buffer::mapAsync):
(WebGPU::Buffer::validateUnmap const):
(WebGPU::Buffer::unmap):
* WebGPU/Device.h:
* WebGPU/Device.mm:
(WebGPU::Device::Device):

Source/WTF:

WTF classes usually have 'using' statements so users don't have to qualify all uses.

* wtf/Range.h:

Modified Paths

Diff

Modified: trunk/Source/WTF/ChangeLog (291368 => 291369)


--- trunk/Source/WTF/ChangeLog	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WTF/ChangeLog	2022-03-16 21:49:48 UTC (rev 291369)
@@ -1,3 +1,14 @@
+2022-03-16  Myles C. Maxfield  <mmaxfi...@apple.com>
+
+        [WebGPU] Implement first draft of buffer mapping according to the spec
+        https://bugs.webkit.org/show_bug.cgi?id=237870
+
+        Reviewed by Kimmo Kinnunen.
+
+        WTF classes usually have 'using' statements so users don't have to qualify all uses.
+
+        * wtf/Range.h:
+
 2022-03-15  Patrick Griffis  <pgrif...@igalia.com>
 
         Add initial implementation of Fetch Metadata

Modified: trunk/Source/WTF/wtf/Range.h (291368 => 291369)


--- trunk/Source/WTF/wtf/Range.h	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WTF/wtf/Range.h	2022-03-16 21:49:48 UTC (rev 291369)
@@ -140,3 +140,5 @@
 };
 
 } // namespace WTF
+
+using WTF::Range;

Modified: trunk/Source/WebGPU/ChangeLog (291368 => 291369)


--- trunk/Source/WebGPU/ChangeLog	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WebGPU/ChangeLog	2022-03-16 21:49:48 UTC (rev 291369)
@@ -1,5 +1,41 @@
 2022-03-16  Myles C. Maxfield  <mmaxfi...@apple.com>
 
+        [WebGPU] Implement first draft of buffer mapping according to the spec
+        https://bugs.webkit.org/show_bug.cgi?id=237870
+
+        Reviewed by Kimmo Kinnunen.
+
+        Implement the various GPUBuffer methods, according to the algorithms in the spec.
+        There are a few things which the spec lists which we can't do yet (like reporting
+        validation errors), so those things are left with FIXMEs. Every step is listed with
+        links to the spec where appropriate, and with quotes to the spec describing what
+        is being implemented.
+
+        * WebGPU.xcodeproj/project.pbxproj:
+        * WebGPU/Buffer.h:
+        (WebGPU::Buffer::create):
+        (WebGPU::Buffer::size const):
+        (WebGPU::Buffer::usage const):
+        * WebGPU/Buffer.mm:
+        (WebGPU::validateDescriptor):
+        (WebGPU::validateCreateBuffer):
+        (WebGPU::storageMode):
+        (WebGPU::Device::createBuffer):
+        (WebGPU::Buffer::Buffer):
+        (WebGPU::Buffer::destroy):
+        (WebGPU::Buffer::getConstMappedRange):
+        (WebGPU::Buffer::validateGetMappedRange const):
+        (WebGPU::Buffer::getMappedRange):
+        (WebGPU::Buffer::validateMapAsync const):
+        (WebGPU::Buffer::mapAsync):
+        (WebGPU::Buffer::validateUnmap const):
+        (WebGPU::Buffer::unmap):
+        * WebGPU/Device.h:
+        * WebGPU/Device.mm:
+        (WebGPU::Device::Device):
+
+2022-03-16  Myles C. Maxfield  <mmaxfi...@apple.com>
+
         [WebGPU] Implement queue submission methods according to the spec
         https://bugs.webkit.org/show_bug.cgi?id=237869
 

Modified: trunk/Source/WebGPU/WebGPU/Buffer.h (291368 => 291369)


--- trunk/Source/WebGPU/WebGPU/Buffer.h	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WebGPU/WebGPU/Buffer.h	2022-03-16 21:49:48 UTC (rev 291369)
@@ -25,19 +25,30 @@
 
 #pragma once
 
+#import <utility>
 #import <wtf/CompletionHandler.h>
 #import <wtf/FastMalloc.h>
+#import <wtf/Range.h>
+#import <wtf/RangeSet.h>
 #import <wtf/Ref.h>
-#import <wtf/RefCounted.h>
+#import <wtf/ThreadSafeRefCounted.h>
 
 namespace WebGPU {
 
-class Buffer : public RefCounted<Buffer> {
+class Device;
+
+class Buffer : public ThreadSafeRefCounted<Buffer> {
     WTF_MAKE_FAST_ALLOCATED;
 public:
-    static Ref<Buffer> create(id<MTLBuffer> buffer)
+    enum class State : uint8_t;
+    struct MappingRange {
+        size_t beginOffset; // Inclusive
+        size_t endOffset; // Exclusive
+    };
+
+    static Ref<Buffer> create(id<MTLBuffer> buffer, uint64_t size, WGPUBufferUsageFlags usage, State initialState, MappingRange initialMappingRange, Device& device)
     {
-        return adoptRef(*new Buffer(buffer));
+        return adoptRef(*new Buffer(buffer, size, usage, initialState, initialMappingRange, device));
     }
 
     ~Buffer();
@@ -49,12 +60,41 @@
     void unmap();
     void setLabel(const char*);
 
+    // https://gpuweb.github.io/gpuweb/#buffer-state
+    // Each GPUBuffer has a current buffer state on the Content timeline which is one of the following:
+    enum class State : uint8_t {
+        Mapped, // "where the GPUBuffer is available for CPU operations on its content."
+        MappedAtCreation, // "where the GPUBuffer was just created and is available for CPU operations on its content."
+        MappingPending, // "where the GPUBuffer is being made available for CPU operations on its content."
+        Unmapped, // "where the GPUBuffer is available for GPU operations."
+        Destroyed, // "where the GPUBuffer is no longer available for any operations except destroy."
+    };
+
     id<MTLBuffer> buffer() const { return m_buffer; }
+    size_t size() const { return m_size; }
+    WGPUBufferUsageFlags usage() const { return m_usage; }
 
 private:
-    Buffer(id<MTLBuffer>);
+    Buffer(id<MTLBuffer>, uint64_t size, WGPUBufferUsageFlags, State initialState, MappingRange initialMappingRange, Device&);
 
+    bool validateGetMappedRange(size_t offset, size_t rangeSize) const;
+    bool validateMapAsync(WGPUMapModeFlags, size_t offset, size_t rangeSize) const;
+    bool validateUnmap() const;
+
     id<MTLBuffer> m_buffer { nil };
+
+    // https://gpuweb.github.io/gpuweb/#buffer-interface
+    // "GPUBuffer has the following internal slots:"
+    const size_t m_size { 0 }; // "The length of the GPUBuffer allocation in bytes."
+    const WGPUBufferUsageFlags m_usage { 0 }; // "The allowed usages for this GPUBuffer."
+    State m_state { State::Unmapped }; // "The current state of the GPUBuffer."
+    // "[[mapping]] of type ArrayBuffer or Promise or null." This is unnecessary; we can just use m_device.contents.
+    MappingRange m_mappingRange { 0, 0 }; // "[[mapping_range]] of type list<unsigned long long> or null."
+    using MappedRanges = RangeSet<Range<size_t>>;
+    MappedRanges m_mappedRanges; // "[[mapped_ranges]] of type list<ArrayBuffer> or null."
+    WGPUMapModeFlags m_mapMode { WGPUMapMode_None }; // "The GPUMapModeFlags of the last call to mapAsync() (if any)."
+
+    const Ref<Device> m_device;
 };
 
 } // namespace WebGPU

Modified: trunk/Source/WebGPU/WebGPU/Buffer.mm (291368 => 291369)


--- trunk/Source/WebGPU/WebGPU/Buffer.mm	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WebGPU/WebGPU/Buffer.mm	2022-03-16 21:49:48 UTC (rev 291369)
@@ -27,17 +27,125 @@
 #import "Buffer.h"
 
 #import "Device.h"
+#import "Utilities.h"
+#import <wtf/StdLibExtras.h>
 
 namespace WebGPU {
 
+static bool validateDescriptor(const Device& device, const WGPUBufferDescriptor& descriptor)
+{
+    UNUSED_PARAM(device);
+
+    // https://gpuweb.github.io/gpuweb/#abstract-opdef-validating-gpubufferdescriptor
+
+    // FIXME: "If device is lost return false."
+
+    // FIXME: "If any of the bits of descriptor’s usage aren’t present in this device’s [[allowed buffer usages]] return false."
+
+    // "If both the MAP_READ and MAP_WRITE bits of descriptor’s usage attribute are set, return false."
+    if ((descriptor.usage & WGPUBufferUsage_MapRead) && (descriptor.usage & WGPUBufferUsage_MapWrite))
+        return false;
+
+    return true;
+}
+
+static bool validateCreateBuffer(const Device& device, const WGPUBufferDescriptor& descriptor)
+{
+    // FIXME: "this is a valid GPUDevice."
+
+    // "validating GPUBufferDescriptor(this, descriptor) returns true."
+    if (!validateDescriptor(device, descriptor))
+        return false;
+
+    // "descriptor.usage must not be 0."
+    if (!descriptor.usage)
+        return false;
+
+    // FIXME: "descriptor.usage is a subset of this.[[allowed buffer usages]]."
+
+    // "If descriptor.usage contains MAP_READ: descriptor.usage contains no other flags except COPY_DST."
+    if ((descriptor.usage & WGPUBufferUsage_MapRead)
+        && (descriptor.usage & ~WGPUBufferUsage_CopyDst & ~WGPUBufferUsage_MapRead))
+        return false;
+
+    // "If descriptor.usage contains MAP_WRITE: descriptor.usage contains no other flags except COPY_SRC."
+    if ((descriptor.usage & WGPUBufferUsage_MapWrite)
+        && (descriptor.usage & ~WGPUBufferUsage_CopySrc & ~WGPUBufferUsage_MapWrite))
+        return false;
+
+    // "If descriptor.mappedAtCreation is true: descriptor.size is a multiple of 4."
+    if (descriptor.mappedAtCreation && (descriptor.size % 4))
+        return false;
+
+    return true;
+}
+
+static MTLStorageMode storageMode(bool deviceHasUnifiedMemory, WGPUBufferUsageFlags usage)
+{
+    if (deviceHasUnifiedMemory)
+        return MTLStorageModeShared;
+    // On discrete memory GPUs, mappable memory is shared, whereas non-mappable memory is private.
+    // There is no situation where we'll use the managed storage mode.
+    if ((usage & WGPUBufferUsage_MapRead) || (usage & WGPUBufferUsage_MapWrite))
+        return MTLStorageModeShared;
+    return MTLStorageModePrivate;
+}
+
 RefPtr<Buffer> Device::createBuffer(const WGPUBufferDescriptor& descriptor)
 {
-    UNUSED_PARAM(descriptor);
-    return Buffer::create(nil);
+    if (descriptor.nextInChain)
+        return nullptr;
+
+    // https://gpuweb.github.io/gpuweb/#dom-gpudevice-createbuffer
+
+    // "If any of the following conditions are unsatisfied, return an error buffer and stop."
+    if (!validateCreateBuffer(*this, descriptor))
+        return nullptr;
+
+    // FIXME: Use heap allocation to make this faster.
+    // If/when we do that, we have to make sure that "new" buffers get cleared to 0.
+    // FIXME: Consider write-combining CPU cache mode.
+    // FIXME: Consider implementing hazard tracking ourself.
+    MTLStorageMode storageMode = WebGPU::storageMode(hasUnifiedMemory(), descriptor.usage);
+    id<MTLBuffer> buffer = [m_device newBufferWithLength:descriptor.size options:storageMode];
+    if (!buffer)
+        return nullptr;
+
+    buffer.label = [NSString stringWithCString:descriptor.label encoding:NSUTF8StringEncoding];
+
+    // FIXME: Handle descriptor.mappedAtCreation.
+    // Because non-mappable buffers can be mapped at creation,
+    // this means we have to have a temporary buffer mapped,
+    // and we can to schedule a copy command when it gets unmapped,
+    // presumably at first use.
+
+    // "Let b be a new GPUBuffer object."
+    // "Set b.[[size]] to descriptor.size."
+    // "Set b.[[usage]] to descriptor.usage."
+
+    // "If descriptor.mappedAtCreation is true:"
+    if (descriptor.mappedAtCreation) {
+        // "Set b.[[mapping]] to a new ArrayBuffer of size b.[[size]]." This is unnecessary.
+        // "Set b.[[mapping_range]] to [0, descriptor.size]."
+        // "Set b.[[mapped_ranges]] to []."
+        // "Set b.[[state]] to mapped at creation."
+        return Buffer::create(buffer, descriptor.size, descriptor.usage, Buffer::State::MappedAtCreation, { static_cast<size_t>(0), descriptor.size }, *this);
+    }
+
+    // "Set b.[[mapping]] to null." This is unnecessary.
+    // "Set b.[[mapping_range]] to null."
+    // "Set b.[[mapped_ranges]] to null."
+    // "Set b.[[state]] to unmapped."
+    return Buffer::create(buffer, descriptor.size, descriptor.usage, Buffer::State::Unmapped, { static_cast<size_t>(0), static_cast<size_t>(0) }, *this);
 }
 
-Buffer::Buffer(id<MTLBuffer> buffer)
+Buffer::Buffer(id<MTLBuffer> buffer, uint64_t size, WGPUBufferUsageFlags usage, State initialState, MappingRange initialMappingRange, Device& device)
     : m_buffer(buffer)
+    , m_size(size)
+    , m_usage(usage)
+    , m_state(initialState)
+    , m_mappingRange(initialMappingRange)
+    , m_device(device)
 {
 }
 
@@ -45,32 +153,233 @@
 
 void Buffer::destroy()
 {
+    // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-destroy
+
+    // "If the this.[[state]] is not either of unmapped or destroyed: Run the steps to unmap this."
+    if (m_state != State::Unmapped && m_state != State::Destroyed) {
+        // FIXME: ASSERT() that this call doesn't fail.
+        unmap();
+    }
+
+    // "Set this.[[state]] to destroyed."
+    m_state = State::Destroyed;
+
+    m_buffer = nil;
 }
 
 const void* Buffer::getConstMappedRange(size_t offset, size_t size)
 {
-    UNUSED_PARAM(offset);
-    UNUSED_PARAM(size);
-    return nullptr;
+    return getMappedRange(offset, size);
 }
 
+bool Buffer::validateGetMappedRange(size_t offset, size_t rangeSize) const
+{
+    // "this.[[state]] is mapped or mapped at creation."
+    if (m_state != State::Mapped && m_state != State::MappedAtCreation)
+        return false;
+
+    // "offset is a multiple of 8."
+    if (offset % 8)
+        return false;
+
+    // "rangeSize is a multiple of 4."
+    if (rangeSize % 4)
+        return false;
+
+    // "offset is greater than or equal to this.[[mapping_range]][0]."
+    if (offset < m_mappingRange.beginOffset)
+        return false;
+
+    // "offset + rangeSize is less than or equal to this.[[mapping_range]][1]."
+    // FIXME: Used checked arithmetic.
+    auto endOffset = offset + rangeSize;
+    if (endOffset > m_mappingRange.endOffset)
+        return false;
+
+    // "[offset, offset + rangeSize) does not overlap another range in this.[[mapped_ranges]]."
+    if (m_mappedRanges.overlaps({ offset, endOffset }))
+        return false;
+
+    return true;
+}
+  
 void* Buffer::getMappedRange(size_t offset, size_t size)
 {
-    UNUSED_PARAM(offset);
-    UNUSED_PARAM(size);
-    return nullptr;
+    // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-getmappedrange
+
+    // "If size is missing: Let rangeSize be max(0, this.[[size]] - offset)."
+    // FIXME: Use checked arithmetic.
+    auto rangeSize = size;
+    if (size == WGPU_WHOLE_MAP_SIZE)
+        rangeSize = std::max(static_cast<size_t>(0), m_size - offset);
+
+    // "If any of the following conditions are unsatisfied"
+    if (!validateGetMappedRange(offset, rangeSize)) {
+        // FIXME: "throw an OperationError and stop."
+        return nullptr;
+    }
+
+    // "Let m be a new ArrayBuffer of size rangeSize pointing at the content of this.[[mapping]] at offset offset - this.[[mapping_range]][0]."
+
+    // "Append m to this.[[mapped_ranges]]."
+    m_mappedRanges.add({ offset, offset + rangeSize });
+
+    // "Return m."
+    return static_cast<char*>(m_buffer.contents) + offset;
 }
 
+bool Buffer::validateMapAsync(WGPUMapModeFlags mode, size_t offset, size_t rangeSize) const
+{
+    // FIXME: "this is a valid GPUBuffer. TODO: check destroyed state?"
+
+    // "offset is a multiple of 8."
+    if (offset % 8)
+        return false;
+
+    // "rangeSize is a multiple of 4."
+    if (rangeSize % 4)
+        return false;
+
+    // "offset + rangeSize is less or equal to this.[[size]]"
+    // FIXME: Use checked arithmetic.
+    if (offset + rangeSize > m_size)
+        return false;
+
+    // "this.[[state]] is unmapped"
+    if (m_state != State::Unmapped)
+        return false;
+
+    // "mode contains exactly one of READ or WRITE."
+    auto readWriteModeFlags = mode & (WGPUMapMode_Read | WGPUMapMode_Write);
+    if (readWriteModeFlags != WGPUMapMode_Read && readWriteModeFlags != WGPUMapMode_Write)
+        return false;
+
+    // "If mode contains READ then this.[[usage]] must contain MAP_READ."
+    if ((mode & WGPUMapMode_Read) && !(m_usage & WGPUBufferUsage_MapRead))
+        return false;
+
+    // "If mode contains WRITE then this.[[usage]] must contain MAP_WRITE."
+    if ((mode & WGPUMapMode_Write) && !(m_usage & WGPUBufferUsage_MapWrite))
+        return false;
+
+    return true;
+}
+
 void Buffer::mapAsync(WGPUMapModeFlags mode, size_t offset, size_t size, CompletionHandler<void(WGPUBufferMapAsyncStatus)>&& callback)
 {
-    UNUSED_PARAM(mode);
-    UNUSED_PARAM(offset);
-    UNUSED_PARAM(size);
-    UNUSED_PARAM(callback);
+    // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-mapasync
+
+    // "If size is missing: Let rangeSize be max(0, this.[[size]] - offset)."
+    // FIXME: Use checked arithmetic.
+    auto rangeSize = size;
+    if (size == WGPU_WHOLE_MAP_SIZE)
+        rangeSize = std::max(static_cast<size_t>(0), m_size - offset);
+
+    // "If any of the following conditions are unsatisfied:"
+    if (!validateMapAsync(mode, offset, rangeSize)) {
+        // FIXME: "Record a validation error on the current scope."
+
+        // "Return a promise rejected with an OperationError on the Device timeline."
+        callback(WGPUBufferMapAsyncStatus_Error);
+        return;
+    }
+
+    // "Set this.[[mapping]] to p."
+
+    // "Set this.[[state]] to mapping pending."
+    m_state = State::MappingPending;
+
+    // "Set this.[[map_mode]] to mode."
+    m_mapMode = mode;
+
+    // "Enqueue an operation on the default queue’s Queue timeline that will execute the following:"
+    m_device->getQueue().onSubmittedWorkDone(0, [protectedThis = Ref { *this }, offset, rangeSize, callback = WTFMove(callback)] (WGPUQueueWorkDoneStatus status) mutable {
+        // "If this.[[state]] is mapping pending:"
+        if (protectedThis->m_state == State::MappingPending) {
+            // "Let m be a new ArrayBuffer of size rangeSize."
+
+            // "Set the content of m to the content of this’s allocation starting at offset offset and for rangeSize bytes."
+
+            // "Set this.[[mapping]] to m."
+
+            // "Set this.[[state]] to mapped."
+            protectedThis->m_state = State::Mapped;
+
+            // "Set this.[[mapping_range]] to [offset, offset + rangeSize]."
+            protectedThis->m_mappingRange = { offset, offset + rangeSize };
+
+            // "Set this.[[mapped_ranges]] to []."
+            protectedThis->m_mappedRanges = MappedRanges();
+        }
+
+        // "Resolve p."
+        switch (status) {
+        case WGPUQueueWorkDoneStatus_Success:
+            callback(WGPUBufferMapAsyncStatus_Success);
+            return;
+        case WGPUQueueWorkDoneStatus_Error:
+            callback(WGPUBufferMapAsyncStatus_Error);
+            return;
+        case WGPUQueueWorkDoneStatus_Unknown:
+            callback(WGPUBufferMapAsyncStatus_Unknown);
+            return;
+        case WGPUQueueWorkDoneStatus_DeviceLost:
+            callback(WGPUBufferMapAsyncStatus_DeviceLost);
+            return;
+        default:
+            ASSERT_NOT_REACHED();
+            callback(WGPUBufferMapAsyncStatus_Error);
+            return;
+        }
+    });
 }
 
+bool Buffer::validateUnmap() const
+{
+    // "this.[[state]] must be mapped at creation, mapping pending, or mapped."
+    if (m_state != State::MappedAtCreation
+        && m_state != State::MappingPending
+        && m_state != State::Mapped)
+        return false;
+
+    return true;
+}
+  
 void Buffer::unmap()
 {
+    // https://gpuweb.github.io/gpuweb/#dom-gpubuffer-unmap
+
+    // "If any of the following requirements are unmet"
+    if (!validateUnmap()) {
+        // FIXME: "generate a validation error and stop."
+        return;
+    }
+
+    // "If this.[[state]] is mapping pending:"
+    if (m_state == State::MappingPending) {
+        // FIXME: "Reject [[mapping]] with an AbortError."
+
+        // "Set this.[[mapping]] to null."
+    }
+
+    // "If this.[[state]] is mapped or mapped at creation:"
+    if (m_state == State::Mapped || m_state == State::MappedAtCreation) {
+        // "If one of the two following conditions holds:"
+        // "this.[[state]] is mapped at creation"
+        // "this.[[state]] is mapped and this.[[map_mode]] contains WRITE"
+        // "Enqueue an operation on the default queue’s Queue timeline that updates the this.[[mapping_range]] of this’s allocation to the content of this.[[mapping]]."
+
+        // "Detach each ArrayBuffer in this.[[mapped_ranges]] from its content."
+
+        // "Set this.[[mapping]] to null."
+
+        // "Set this.[[mapping_range]] to null."
+
+        // "Set this.[[mapped_ranges]] to null."
+    }
+
+    // "Set this.[[state]] to unmapped."
+    m_state = State::Unmapped;
 }
 
 void Buffer::setLabel(const char* label)

Modified: trunk/Source/WebGPU/WebGPU/Device.h (291368 => 291369)


--- trunk/Source/WebGPU/WebGPU/Device.h	2022-03-16 21:35:36 UTC (rev 291368)
+++ trunk/Source/WebGPU/WebGPU/Device.h	2022-03-16 21:49:48 UTC (rev 291369)
@@ -85,6 +85,7 @@
     void setLabel(const char*);
 
     Instance& instance() { return m_instance; }
+    bool hasUnifiedMemory() const { return m_device.hasUnifiedMemory; }
 
 private:
     Device(id<MTLDevice>, id<MTLCommandQueue> defaultQueue, Instance&);
_______________________________________________
webkit-changes mailing list
webkit-changes@lists.webkit.org
https://lists.webkit.org/mailman/listinfo/webkit-changes

Reply via email to