Commit b8c031c0 authored by Corentin Wallez's avatar Corentin Wallez Committed by Commit Bot

Roll the WebGPU CTS

Also changes regenerate_internal_cts_html to have slightly more debug
outputs and use `npm script` instead of `npm exec`.

Bug: None
Change-Id: Ifee95c5388cb562d3a5da4590ae3ef5402853f27
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2534730
Commit-Queue: Corentin Wallez <cwallez@chromium.org>
Reviewed-by: default avatarKai Ninomiya <kainino@chromium.org>
Cr-Commit-Position: refs/heads/master@{#827243}
parent 81b83e03
...@@ -1493,7 +1493,7 @@ deps = { ...@@ -1493,7 +1493,7 @@ deps = {
Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '1a111c2adcb4898655aacaca018d6e275172760b', Var('chromium_git') + '/external/khronosgroup/webgl.git' + '@' + '1a111c2adcb4898655aacaca018d6e275172760b',
'src/third_party/webgpu-cts/src': 'src/third_party/webgpu-cts/src':
Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + 'ce55078b90c44b8e304f77b1e72a90bb76d99658', Var('chromium_git') + '/external/github.com/gpuweb/cts.git' + '@' + '7518ac184c13bc1d7d68309d03234c834682428e',
'src/third_party/blink/web_tests/wpt_internal/webgpu/third_party/glslang_js': { 'src/third_party/blink/web_tests/wpt_internal/webgpu/third_party/glslang_js': {
'packages': [ 'packages': [
......
...@@ -31,12 +31,13 @@ wpt_internal/webgpu/cts.html?q=webgpu:api,validation,copyTextureToTexture:textur ...@@ -31,12 +31,13 @@ wpt_internal/webgpu/cts.html?q=webgpu:api,validation,copyTextureToTexture:textur
wpt_internal/webgpu/cts.html?q=webgpu:api,operation,buffers,map_oom:mappedAtCreation,smaller_getMappedRange:* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,operation,buffers,map_oom:mappedAtCreation,smaller_getMappedRange:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,operation,render_pass,storeOp:* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,operation,render_pass,storeOp:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroup:buffer_binding_must_contain_exactly_one_buffer_of_its_type,* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroup:texture_binding_must_have_correct_usage,* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroupLayout:bindingTypeSpecific_optional_members,* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroupLayout:bindingTypeSpecific_optional_members,* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroupLayout:visibility,* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroupLayout:visibility,* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,render_pass,storeOp:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,copyBufferToBuffer:copy_with_invalid_buffer: [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,copyBufferToBuffer:copy_with_invalid_buffer: [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,createBindGroup:buffer_binding_must_contain_exactly_one_buffer_of_its_type,* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,encoding,cmds,compute_pass:* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,encoding,cmds,compute_pass:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,render_pass,storeOp:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,buffer_mapping:mapAsync,invalidBuffer:* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,buffer_mapping:mapAsync,invalidBuffer:* [ Failure ]
wpt_internal/webgpu/cts.html?q=webgpu:api,validation,buffer_mapping:mapAsync,offsetAndSizeAlignment,* [ Failure ] wpt_internal/webgpu/cts.html?q=webgpu:api,validation,buffer_mapping:mapAsync,offsetAndSizeAlignment,* [ Failure ]
......
...@@ -26,17 +26,19 @@ echo $expectations ...@@ -26,17 +26,19 @@ echo $expectations
pushd third_party/blink > /dev/null pushd third_party/blink > /dev/null
echo 'Extracting expectation names...'
tools/extract_expectation_names.py web_tests/WebGPUExpectations > $expectations tools/extract_expectation_names.py web_tests/WebGPUExpectations > $expectations
popd > /dev/null popd > /dev/null
pushd third_party/webgpu-cts/src > /dev/null pushd third_party/webgpu-cts/src > /dev/null
echo 'Updating node for webgpu-cts...'
npm install --frozen-lockfile npm install --frozen-lockfile
npx grunt run:generate-listings npx grunt run:generate-listings
echo 'Regenerating...' echo 'Regenerating expectations...'
npx ./tools/gen_wpt_cts_html \ npm run gen_wpt_cts_html \
../../blink/web_tests/wpt_internal/webgpu/cts.html \ ../../blink/web_tests/wpt_internal/webgpu/cts.html \
../../blink/web_tests/webgpu/ctshtml-template.txt \ ../../blink/web_tests/webgpu/ctshtml-template.txt \
../../blink/web_tests/webgpu/argsprefixes.txt \ ../../blink/web_tests/webgpu/argsprefixes.txt \
......
...@@ -2,7 +2,9 @@ ...@@ -2,7 +2,9 @@
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ import { assert, unreachable } from './util/util.js'; **/ import { assert, unreachable } from './util/util.js';
/** Whether we've tried to load glslang or not. */
let glslangAttempted = false; let glslangAttempted = false;
/** Glslang instance if it has loaded, undefined if it hasn't (or failed). */
let glslangInstance; let glslangInstance;
export async function initGLSL() { export async function initGLSL() {
......
/**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ export class AsyncMutex {
// The newest item currently waiting for the mutex. This promise is chained so
// that it implicitly defines a FIFO queue where this is the "last-in" item.
// Run an async function with a lock on this mutex.
// Waits until the mutex is available, locks it, runs the function, then releases it.
async with(fn) {
const p = (async () => {
// If the mutex is locked, wait for the last thing in the queue before running.
// (Everything in the queue runs in order, so this is after everything currently enqueued.)
if (this.newestQueueItem) {
await this.newestQueueItem;
}
return fn();
})();
// Push the newly-created Promise onto the queue by replacing the old "newest" item.
this.newestQueueItem = p;
// And return so the caller can wait on the result.
return p;
}
}
// AUTO-GENERATED - DO NOT EDIT. See tools/gen_version. // AUTO-GENERATED - DO NOT EDIT. See tools/gen_version.
export const version = 'ce55078b90c44b8e304f77b1e72a90bb76d99658'; export const version = '7518ac184c13bc1d7d68309d03234c834682428e-dirty';
/** /**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts * AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ import { DefaultTestFileLoader } from '../framework/file_loader.js'; **/ // Implements the wpt-embedded test runner (see also: wpt/cts.html).
import { DefaultTestFileLoader } from '../framework/file_loader.js';
import { Logger } from '../framework/logging/logger.js'; import { Logger } from '../framework/logging/logger.js';
import { parseQuery } from '../framework/query/parseQuery.js'; import { parseQuery } from '../framework/query/parseQuery.js';
import { AsyncMutex } from '../framework/util/async_mutex.js';
import { assert } from '../framework/util/util.js'; import { assert } from '../framework/util/util.js';
import { optionEnabled } from './helper/options.js'; import { optionEnabled } from './helper/options.js';
import { TestWorker } from './helper/test_worker.js'; import { TestWorker } from './helper/test_worker.js';
// testharness.js API (https://web-platform-tests.org/writing-tests/testharness-api.html)
setup({
// It's convenient for us to asynchronously add tests to the page. Prevent done() from being
// called implicitly when the page is finished loading.
explicit_done: true,
});
(async () => { (async () => {
const loader = new DefaultTestFileLoader(); const loader = new DefaultTestFileLoader();
const qs = new URLSearchParams(window.location.search).getAll('q'); const qs = new URLSearchParams(window.location.search).getAll('q');
assert(qs.length === 1, 'currently, there must be exactly one ?q='); assert(qs.length === 1, 'currently, there must be exactly one ?q=');
const testcases = await loader.loadCases(parseQuery(qs[0])); const testcases = await loader.loadCases(parseQuery(qs[0]));
await addWPTTests(testcases);
})();
// Note: `async_test`s must ALL be added within the same task. This function *must not* be async.
function addWPTTests(testcases) {
const worker = optionEnabled('worker') ? new TestWorker(false) : undefined; const worker = optionEnabled('worker') ? new TestWorker(false) : undefined;
const log = new Logger(false); const log = new Logger(false);
const mutex = new AsyncMutex();
const running = [];
for (const testcase of testcases) { for (const testcase of testcases) {
const name = testcase.query.toString(); const name = testcase.query.toString();
const wpt_fn = function () { const wpt_fn = async t => {
const p = mutex.with(async () => { const [rec, res] = log.record(name);
const [rec, res] = log.record(name); if (worker) {
if (worker) { await worker.run(rec, name);
await worker.run(rec, name); } else {
} else { await testcase.run(rec);
await testcase.run(rec); }
t.step(() => {
// Unfortunately, it seems not possible to surface any logs for warn/skip.
if (res.status === 'fail') {
throw (res.logs || []).map(s => s.toJSON()).join('\n\n');
} }
this.step(() => {
// Unfortunately, it seems not possible to surface any logs for warn/skip.
if (res.status === 'fail') {
throw (res.logs || []).map(s => s.toJSON()).join('\n\n');
}
});
this.done();
}); });
running.push(p);
}; };
async_test(wpt_fn, name); promise_test(wpt_fn, name);
} }
return Promise.all(running).then(() => log); done();
} })();
...@@ -111,6 +111,7 @@ ...@@ -111,6 +111,7 @@
<meta name=variant content='?q=webgpu:api,validation,createBindGroupLayout:max_resources_per_stage,in_pipeline_layout:*'> <meta name=variant content='?q=webgpu:api,validation,createBindGroupLayout:max_resources_per_stage,in_pipeline_layout:*'>
<meta name=variant content='?q=webgpu:api,validation,createPipelineLayout:*'> <meta name=variant content='?q=webgpu:api,validation,createPipelineLayout:*'>
<meta name=variant content='?q=webgpu:api,validation,createRenderPipeline:*'> <meta name=variant content='?q=webgpu:api,validation,createRenderPipeline:*'>
<meta name=variant content='?q=webgpu:api,validation,createSampler:*'>
<meta name=variant content='?q=webgpu:api,validation,createTexture:*'> <meta name=variant content='?q=webgpu:api,validation,createTexture:*'>
<meta name=variant content='?q=webgpu:api,validation,createView:*'> <meta name=variant content='?q=webgpu:api,validation,createView:*'>
<meta name=variant content='?q=webgpu:api,validation,encoding,cmds,compute_pass:*'> <meta name=variant content='?q=webgpu:api,validation,encoding,cmds,compute_pass:*'>
......
...@@ -289,7 +289,7 @@ class PrimitiveTopologyTest extends GPUTest { ...@@ -289,7 +289,7 @@ class PrimitiveTopologyTest extends GPUTest {
GPUBufferUsage.INDEX GPUBufferUsage.INDEX
); );
renderPass.setIndexBuffer(indexBuffer, 0); renderPass.setIndexBuffer(indexBuffer, 'uint32');
renderPass.drawIndexed(7, 1, 0, 0, 0); // extra index for restart renderPass.drawIndexed(7, 1, 0, 0, 0); // extra index for restart
} else { } else {
renderPass.draw(6, 1, 0, 0); renderPass.draw(6, 1, 0, 0);
......
...@@ -81,7 +81,6 @@ g.test('bindingTypeSpecific_optional_members') ...@@ -81,7 +81,6 @@ g.test('bindingTypeSpecific_optional_members')
...pbool('hasDynamicOffset'), ...pbool('hasDynamicOffset'),
...poptions('minBufferBindingSize', [0, 4]), ...poptions('minBufferBindingSize', [0, 4]),
...poptions('textureComponentType', kTextureComponentTypes), ...poptions('textureComponentType', kTextureComponentTypes),
...pbool('multisampled'),
...poptions('viewDimension', kTextureViewDimensions), ...poptions('viewDimension', kTextureViewDimensions),
...poptions('storageTextureFormat', kAllTextureFormats), ...poptions('storageTextureFormat', kAllTextureFormats),
]) ])
...@@ -92,7 +91,6 @@ g.test('bindingTypeSpecific_optional_members') ...@@ -92,7 +91,6 @@ g.test('bindingTypeSpecific_optional_members')
hasDynamicOffset, hasDynamicOffset,
minBufferBindingSize, minBufferBindingSize,
textureComponentType, textureComponentType,
multisampled,
viewDimension, viewDimension,
storageTextureFormat, storageTextureFormat,
} = t.params; } = t.params;
...@@ -107,7 +105,6 @@ g.test('bindingTypeSpecific_optional_members') ...@@ -107,7 +105,6 @@ g.test('bindingTypeSpecific_optional_members')
} }
if (kBindingTypeInfo[type].resource !== 'sampledTex') { if (kBindingTypeInfo[type].resource !== 'sampledTex') {
success && (success = textureComponentType === undefined); success && (success = textureComponentType === undefined);
success && (success = multisampled === undefined);
} }
if (kBindingTypeInfo[type].resource !== 'storageTex') { if (kBindingTypeInfo[type].resource !== 'storageTex') {
success && (success = storageTextureFormat === undefined); success && (success = storageTextureFormat === undefined);
...@@ -130,7 +127,6 @@ g.test('bindingTypeSpecific_optional_members') ...@@ -130,7 +127,6 @@ g.test('bindingTypeSpecific_optional_members')
hasDynamicOffset, hasDynamicOffset,
minBufferBindingSize, minBufferBindingSize,
textureComponentType, textureComponentType,
multisampled,
viewDimension, viewDimension,
storageTextureFormat, storageTextureFormat,
}, },
...@@ -140,15 +136,11 @@ g.test('bindingTypeSpecific_optional_members') ...@@ -140,15 +136,11 @@ g.test('bindingTypeSpecific_optional_members')
}); });
g.test('multisample_requires_2d_view_dimension') g.test('multisample_requires_2d_view_dimension')
.params( .params(params().combine(poptions('viewDimension', [undefined, ...kTextureViewDimensions])))
params()
.combine(poptions('multisampled', [undefined, false, true]))
.combine(poptions('viewDimension', [undefined, ...kTextureViewDimensions]))
)
.fn(async t => { .fn(async t => {
const { multisampled, viewDimension } = t.params; const { viewDimension } = t.params;
const success = multisampled !== true || viewDimension === '2d' || viewDimension === undefined; const success = viewDimension === '2d' || viewDimension === undefined;
t.expectValidationError(() => { t.expectValidationError(() => {
t.device.createBindGroupLayout({ t.device.createBindGroupLayout({
...@@ -156,8 +148,7 @@ g.test('multisample_requires_2d_view_dimension') ...@@ -156,8 +148,7 @@ g.test('multisample_requires_2d_view_dimension')
{ {
binding: 0, binding: 0,
visibility: GPUShaderStage.COMPUTE, visibility: GPUShaderStage.COMPUTE,
type: 'sampled-texture', type: 'multisampled-texture',
multisampled,
viewDimension, viewDimension,
}, },
], ],
......
/**
* AUTO-GENERATED - DO NOT EDIT. Source: https://github.com/gpuweb/cts
**/ export const description = `
createSampler validation tests.
`;
import { poptions, params } from '../../../common/framework/params_builder.js';
import { makeTestGroup } from '../../../common/framework/test_group.js';
import { ValidationTest } from './validation_test.js';
export const g = makeTestGroup(ValidationTest);
g.test('lodMinAndMaxClamp')
.desc('test different combinations of min and max clamp values')
.params(
params()
.combine(poptions('lodMinClamp', [-4e-30, -1, 0, 0.5, 1, 10, 4e30]))
.combine(poptions('lodMaxClamp', [-4e-30, -1, 0, 0.5, 1, 10, 4e30]))
)
.fn(async t => {
t.expectValidationError(() => {
t.device.createSampler({
lodMinClamp: t.params.lodMinClamp,
lodMaxClamp: t.params.lodMaxClamp,
});
}, t.params.lodMinClamp > t.params.lodMaxClamp || t.params.lodMinClamp < 0 || t.params.lodMaxClamp < 0);
});
...@@ -79,7 +79,7 @@ class F extends ValidationTest { ...@@ -79,7 +79,7 @@ class F extends ValidationTest {
const encoder = this.device.createCommandEncoder(); const encoder = this.device.createCommandEncoder();
const pass = this.beginRenderPass(encoder); const pass = this.beginRenderPass(encoder);
pass.setPipeline(pipeline); pass.setPipeline(pipeline);
pass.setIndexBuffer(indexBuffer); pass.setIndexBuffer(indexBuffer, 'uint32');
pass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance); pass.drawIndexed(indexCount, instanceCount, firstIndex, baseVertex, firstInstance);
pass.endPass(); pass.endPass();
...@@ -103,7 +103,7 @@ class F extends ValidationTest { ...@@ -103,7 +103,7 @@ class F extends ValidationTest {
const encoder = this.device.createCommandEncoder(); const encoder = this.device.createCommandEncoder();
const pass = this.beginRenderPass(encoder); const pass = this.beginRenderPass(encoder);
pass.setPipeline(pipeline); pass.setPipeline(pipeline);
pass.setIndexBuffer(indexBuffer, 0); pass.setIndexBuffer(indexBuffer, 'uint32');
pass.drawIndexedIndirect(indirectBuffer, indirectOffset); pass.drawIndexedIndirect(indirectBuffer, indirectOffset);
pass.endPass(); pass.endPass();
......
...@@ -334,7 +334,19 @@ export const kRegularTextureFormatInfo = { ...@@ -334,7 +334,19 @@ export const kRegularTextureFormatInfo = {
blockWidth: 1, blockWidth: 1,
blockHeight: 1, blockHeight: 1,
}, },
rg11b10float: { rg11b10ufloat: {
renderable: false,
color: true,
depth: false,
stencil: false,
storage: false,
copySrc: true,
copyDst: true,
bytesPerBlock: 4,
blockWidth: 1,
blockHeight: 1,
},
rgb9e5ufloat: {
renderable: false, renderable: false,
color: true, color: true,
depth: false, depth: false,
...@@ -649,7 +661,7 @@ export const kCompressedTextureFormatInfo = { ...@@ -649,7 +661,7 @@ export const kCompressedTextureFormatInfo = {
blockHeight: 4, blockHeight: 4,
extension: 'texture-compression-bc', extension: 'texture-compression-bc',
}, },
'bc6h-rgb-sfloat': { 'bc6h-rgb-float': {
renderable: false, renderable: false,
color: true, color: true,
depth: false, depth: false,
...@@ -766,6 +778,7 @@ export const kTextureComponentTypeInfo = { ...@@ -766,6 +778,7 @@ export const kTextureComponentTypeInfo = {
float: {}, float: {},
sint: {}, sint: {},
uint: {}, uint: {},
'depth-comparison': {},
}; };
export const kTextureComponentTypes = keysOf(kTextureComponentTypeInfo); export const kTextureComponentTypes = keysOf(kTextureComponentTypeInfo);
...@@ -896,6 +909,11 @@ export const kTextureBindingTypeInfo = { ...@@ -896,6 +909,11 @@ export const kTextureBindingTypeInfo = {
...kBindingKind.sampledTex, ...kBindingKind.sampledTex,
...kValidStagesAll, ...kValidStagesAll,
}, },
'multisampled-texture': {
usage: GPUConst.TextureUsage.SAMPLED,
...kBindingKind.sampledTex,
...kValidStagesAll,
},
'writeonly-storage-texture': { 'writeonly-storage-texture': {
usage: GPUConst.TextureUsage.STORAGE, usage: GPUConst.TextureUsage.STORAGE,
...kBindingKind.storageTex, ...kBindingKind.storageTex,
......
...@@ -286,6 +286,14 @@ export const listing = [ ...@@ -286,6 +286,14 @@ export const listing = [
], ],
"description": "createRenderPipeline validation tests." "description": "createRenderPipeline validation tests."
}, },
{
"file": [
"api",
"validation",
"createSampler"
],
"description": "createSampler validation tests."
},
{ {
"file": [ "file": [
"api", "api",
......
...@@ -106,7 +106,7 @@ class DrawCall { ...@@ -106,7 +106,7 @@ class DrawCall {
// Insert an indexed draw call into |pass| // Insert an indexed draw call into |pass|
drawIndexed(pass) { drawIndexed(pass) {
this.bindVertexBuffers(pass); this.bindVertexBuffers(pass);
pass.setIndexBuffer(this.indexBuffer); pass.setIndexBuffer(this.indexBuffer, 'uint16');
pass.drawIndexed( pass.drawIndexed(
this.indexCount, this.indexCount,
this.instanceCount, this.instanceCount,
...@@ -125,7 +125,7 @@ class DrawCall { ...@@ -125,7 +125,7 @@ class DrawCall {
// Insert an indexed indirect draw call into |pass| // Insert an indexed indirect draw call into |pass|
drawIndexedIndirect(pass) { drawIndexedIndirect(pass) {
this.bindVertexBuffers(pass); this.bindVertexBuffers(pass);
pass.setIndexBuffer(this.indexBuffer); pass.setIndexBuffer(this.indexBuffer, 'uint16');
pass.drawIndexedIndirect(this.generateIndexedIndirectBuffer(), 0); pass.drawIndexedIndirect(this.generateIndexedIndirectBuffer(), 0);
} }
......
...@@ -106,6 +106,13 @@ const float32 = { ...@@ -106,6 +106,13 @@ const float32 = {
bitLength: 32, bitLength: 32,
}; };
const componentUnimplemented = {
write: () => {
unreachable('TexelComponentInfo not implemented for this texture format');
},
bitLength: 0,
};
const repeatComponents = (componentOrder, perComponentInfo) => { const repeatComponents = (componentOrder, perComponentInfo) => {
const componentInfo = componentOrder.reduce((acc, curr) => { const componentInfo = componentOrder.reduce((acc, curr) => {
return Object.assign(acc, { return Object.assign(acc, {
...@@ -160,11 +167,21 @@ const kRepresentationInfo = { ...@@ -160,11 +167,21 @@ const kRepresentationInfo = {
componentInfo: { R: unorm10, G: unorm10, B: unorm10, A: unorm2 }, componentInfo: { R: unorm10, G: unorm10, B: unorm10, A: unorm2 },
sRGB: false, sRGB: false,
}, },
rg11b10float: { rg11b10ufloat: {
componentOrder: kRGB, componentOrder: kRGB,
componentInfo: { R: float11, G: float11, B: float10 }, componentInfo: { R: float11, G: float11, B: float10 },
sRGB: false, sRGB: false,
}, },
// TODO: the e5 is shared between all components; figure out how to write it.
rgb9e5ufloat: {
componentOrder: kRGB,
componentInfo: {
R: componentUnimplemented,
G: componentUnimplemented,
B: componentUnimplemented,
},
sRGB: false,
},
depth32float: { depth32float: {
componentOrder: [TexelComponent.Depth], componentOrder: [TexelComponent.Depth],
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment