diff --git a/webnn/conformance_tests/buffer.https.any.js b/webnn/conformance_tests/buffer.https.any.js index 5b0f46dae02112..af816edb73aaea 100644 --- a/webnn/conformance_tests/buffer.https.any.js +++ b/webnn/conformance_tests/buffer.https.any.js @@ -9,4 +9,8 @@ testCreateWebNNBuffer("create", 4); -testDestroyWebNNBuffer("destroyTwice"); \ No newline at end of file +testDestroyWebNNBuffer("destroyTwice"); + +testReadWebNNBuffer("read"); + +testWriteWebNNBuffer("write"); diff --git a/webnn/conformance_tests/gpu/buffer.https.any.js b/webnn/conformance_tests/gpu/buffer.https.any.js index 66bba9ef4afc66..c470e5d2d653a4 100644 --- a/webnn/conformance_tests/gpu/buffer.https.any.js +++ b/webnn/conformance_tests/gpu/buffer.https.any.js @@ -9,4 +9,8 @@ testCreateWebNNBuffer("create", 4, 'gpu'); -testDestroyWebNNBuffer("destroyTwice", 'gpu'); \ No newline at end of file +testDestroyWebNNBuffer("destroyTwice", 'gpu'); + +testReadWebNNBuffer("read", 'gpu'); + +testWriteWebNNBuffer("write", 'gpu'); diff --git a/webnn/resources/utils.js b/webnn/resources/utils.js index 375c71174a8d11..1c0f9f8d531b6e 100644 --- a/webnn/resources/utils.js +++ b/webnn/resources/utils.js @@ -931,6 +931,7 @@ const toHalf = (value) => { * WebNN buffer creation. * @param {MLContext} context - the context used to create the buffer. * @param {Number} bufferSize - Size of the buffer to create, in bytes. + * @returns {MLBuffer} the created buffer. */ const createBuffer = (context, bufferSize) => { let buffer; @@ -980,4 +981,185 @@ const testCreateWebNNBuffer = (testName, bufferSize, deviceType = 'cpu') => { promise_test(async () => { createBuffer(context, bufferSize); }, `${testName} / ${bufferSize}`); +}; + +/** + * Asserts the buffer data in MLBuffer matches expected. + * @param {MLContext} ml_context - The context used to create the buffer. + * @param {MLBuffer} ml_buffer - The buffer to read and compare data. + * @param {Array} expected - Array of the expected data in the buffer. + */ +const assert_buffer_data_equals = async (ml_context, ml_buffer, expected) => { + const actual = await ml_context.readBuffer(ml_buffer); + assert_array_equals(new expected.constructor(actual), expected, "Read buffer data equals expected data."); +}; + +/** + * Helper to construct a typed array from array buffer with data. + * @param {ArrayBuffer} array_buffer - The array buffer to create a view from. + * @param {Array} array_data - The byte array used to initialize the view. + * @param {String} [type="uint8"] - The data type of the typed array to return. + * @return {TypedArray} - Array view to return of type. + */ +const create_array_view = (array_buffer, array_data, type = 'uint8') => { + let typed_array = new TypedArrayDict[type](array_buffer); + typed_array.set(array_data); + return typed_array; +} + +/** + * WebNN write buffer operation test. + * @param {String} testName - The name of the test operation. + * @param {String} deviceType - The execution device type for this test. + */ +const testWriteWebNNBuffer = (testName, deviceType = 'cpu') => { + let ml_context; + promise_setup(async () => { + ml_context = await navigator.ml.createContext({deviceType}); + }); + + promise_test(async () => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + let array_buffer = new ArrayBuffer(ml_buffer.size); + + // Writing the full buffer. + let input_data = [0xAA, 0xAA, 0xAA, 0xAA]; + ml_context.writeBuffer(ml_buffer, create_array_view(array_buffer, input_data)); + assert_buffer_data_equals(ml_context, ml_buffer, Uint8Array.from(input_data)); + + ml_context.writeBuffer(ml_buffer, new Uint32Array(array_buffer)); + assert_buffer_data_equals(ml_context, ml_buffer, Uint32Array.from(input_data)); + + ml_context.writeBuffer(ml_buffer, create_array_view(array_buffer, [0xBBBB], 'uint32')); + assert_buffer_data_equals(ml_context, ml_buffer, Uint32Array.from([0xBBBB])); + + // Writing to the remainder of the buffer from source offset. + ml_context.writeBuffer(ml_buffer, create_array_view(array_buffer, [0xAA, 0xAA, 0xBB, 0xBB]), /*srcOffset=*/2); + + // Writing zero bytes at the end of the buffer. + ml_context.writeBuffer(ml_buffer, new Uint32Array(array_buffer), /*srcOffset=*/1); + + assert_buffer_data_equals(ml_context, ml_buffer, Uint8Array.from([0xBB, 0xBB, 0xAA, 0xAA])); + + // Writing with both a source offset and size. + ml_context.writeBuffer(ml_buffer, create_array_view(array_buffer, [0xDD, 0xDD, 0xCC, 0xDD]), /*srcOffset=*/2, /*srcSize=*/1); + assert_buffer_data_equals(ml_context, ml_buffer, Uint8Array.from([0xCC, 0xBB, 0xAA, 0xAA])); + + // Writing from a larger source buffer that allows it to fit in the buffer. + ml_context.writeBuffer(ml_buffer, Uint8Array.from([0xAA, 0xBB, 0xCC, 0xDD, 0xEE])); + assert_buffer_data_equals(ml_context, ml_buffer, Uint8Array.from([0xAA, 0xBB, 0xCC, 0xDD])); + + // Writing with a size that goes past that source buffer length. + assert_throws_js(TypeError, () => ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/0, /*srcSize=*/input_data.length + 1)); + assert_throws_js(TypeError, () => ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/3, /*srcSize=*/4)); + + // Writing with a source offset that is out of range of the source size. + assert_throws_js(TypeError, () => ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/input_data.length + 1)); + + // Writing with a source offset that is out of range of implicit copy size. + assert_throws_js(TypeError, () => ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/input_data.length + 1, /*srcSize=*/undefined)); + + // Writing with a source offset of undefined should be treated as 0. + ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/undefined, /*srcSize=*/input_data.length); + assert_buffer_data_equals(ml_context, ml_buffer, Uint8Array.from(input_data)); + + assert_throws_js(TypeError, () => ml_context.writeBuffer(ml_buffer, new Uint8Array(array_buffer), /*srcOffset=*/undefined, /*srcSize=*/input_data.length + 1)); + + }, `${testName}`); + + promise_test(async () => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + // Writing data to a destroyed MLBuffer should throw. + ml_buffer.destroy(); + + assert_throws_dom("InvalidStateError", () => ml_context.writeBuffer(ml_buffer, new Uint8Array(ml_buffer.size))); + + }, `${testName} / destroy`); + + promise_test(async () => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + let another_ml_context = await navigator.ml.createContext({deviceType}); + let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); + + let input_data = new Uint8Array(ml_buffer.size).fill(0xAA); + assert_throws_js(TypeError, () => ml_context.writeBuffer(another_ml_buffer, input_data)); + assert_throws_js(TypeError, () => another_ml_context.writeBuffer(ml_buffer, input_data)); + + }, `${testName} / context_mismatch`); + + promise_test(async () => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + const array_buffer = new ArrayBuffer(ml_buffer.size); + const detached_buffer = array_buffer.transfer(); + assert_true(array_buffer.detached, "array buffer should be detached."); + + ml_context.writeBuffer(ml_buffer, array_buffer); + + }, `${testName} / detached`); +}; + +/** + * WebNN read buffer operation test. + * @param {String} testName - The name of the test operation. + * @param {String} deviceType - The execution device type for this test. + */ +const testReadWebNNBuffer = (testName, deviceType = 'cpu') => { + let ml_context; + promise_setup(async () => { + ml_context = await navigator.ml.createContext({deviceType}); + }); + + promise_test(async t => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + // Reading a destroyed MLBuffer should reject. + ml_buffer.destroy(); + + promise_rejects_dom(t, "InvalidStateError", ml_context.readBuffer(ml_buffer)); + }, `${testName} / destroy`); + + promise_test(async t => { + let ml_buffer = createBuffer(ml_context, 4); + + // MLBuffer was unsupported for the deviceType. + if (ml_buffer === undefined){ + return; + } + + let another_ml_context = await navigator.ml.createContext({deviceType}); + let another_ml_buffer = createBuffer(another_ml_context, ml_buffer.size); + + promise_rejects_js(t, TypeError, ml_context.readBuffer(another_ml_buffer)); + promise_rejects_js(t, TypeError, another_ml_context.readBuffer(ml_buffer)); + + }, `${testName} / context_mismatch`); }; \ No newline at end of file