Commit dfdc8f68 authored by Manos Koukoutos's avatar Manos Koukoutos Committed by V8 LUCI CQ

[wasm-gc] Implement array.copy (experimental)

Changes:
- Add --experimental-wasm-gc-experiments flag.
- Add array.copy opcode. Implement it in decoding and code generation
  behind the new flag.
- Add WasmCodeBuilder::BoundsCheckArrayCopy. Move BoundsCheckArray to
  the private section.
- Add WasmArrayCopy and WasmArrayCopyWithChecks builtin.
- Add WasmArrayCopy runtime function.
- Add WasmArray::ElementSlot.
- Always print two hex digits in CHECK_PROTOTYPE_OPCODE.
- In test-gc, print the thrown-error message if the function should not
  throw.
- In test-gc, add GetResultObject with one argument.

Bug: v8:7748
Change-Id: I58f4d37e254154596cdef5e78482b55260dd3782
Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/2912729
Commit-Queue: Manos Koukoutos <manoskouk@chromium.org>
Reviewed-by: 's avatarJakob Kummerow <jkummerow@chromium.org>
Cr-Commit-Position: refs/heads/master@{#74806}
parent 2243a863
......@@ -35,6 +35,8 @@ extern runtime WasmI32AtomicWait(
extern runtime WasmI64AtomicWait(
Context, WasmInstanceObject, Number, BigInt, BigInt): Smi;
extern runtime WasmAllocateRtt(Context, Smi, Map): Map;
extern runtime WasmArrayCopy(
Context, WasmArray, Smi, WasmArray, Smi, Smi): JSAny;
}
namespace unsafe {
......@@ -315,6 +317,36 @@ builtin WasmAllocateArrayWithRtt(
return result;
}
// We put all uint32 parameters at the beginning so that they are assigned to
// registers.
builtin WasmArrayCopyWithChecks(
dstIndex: uint32, srcIndex: uint32, length: uint32, dstObject: Object,
srcObject: Object): JSAny {
if (dstObject == Null) tail ThrowWasmTrapNullDereference();
if (srcObject == Null) tail ThrowWasmTrapNullDereference();
const dstArray = %RawDownCast<WasmArray>(dstObject);
const srcArray = %RawDownCast<WasmArray>(srcObject);
// Check that the end of the copying range is in-bounds and that the range
// does not overflow.
if (dstIndex + length > dstArray.length || dstIndex + length < dstIndex ||
srcIndex + length > srcArray.length || srcIndex + length < srcIndex) {
tail ThrowWasmTrapArrayOutOfBounds();
}
tail runtime::WasmArrayCopy(
LoadContextFromFrame(), dstArray, SmiFromUint32(dstIndex), srcArray,
SmiFromUint32(srcIndex), SmiFromUint32(length));
}
// We put all uint32 parameters at the beginning so that they are assigned to
// registers.
builtin WasmArrayCopy(
dstIndex: uint32, srcIndex: uint32, length: uint32, dstArray: WasmArray,
srcArray: WasmArray): JSAny {
tail runtime::WasmArrayCopy(
LoadContextFromFrame(), dstArray, SmiFromUint32(dstIndex), srcArray,
SmiFromUint32(srcIndex), SmiFromUint32(length));
}
// Redeclaration with different typing (value is an Object, not JSAny).
extern transitioning runtime
CreateDataProperty(implicit context: Context)(JSReceiver, JSAny, Object);
......
......@@ -5903,13 +5903,24 @@ void WasmGraphBuilder::StructSet(Node* struct_object,
gasm_->StoreStructField(struct_object, struct_type, field_index, field_value);
}
void WasmGraphBuilder::BoundsCheck(Node* array, Node* index,
wasm::WasmCodePosition position) {
void WasmGraphBuilder::BoundsCheckArray(Node* array, Node* index,
wasm::WasmCodePosition position) {
Node* length = gasm_->LoadWasmArrayLength(array);
TrapIfFalse(wasm::kTrapArrayOutOfBounds, gasm_->Uint32LessThan(index, length),
position);
}
void WasmGraphBuilder::BoundsCheckArrayCopy(Node* array, Node* index,
Node* length,
wasm::WasmCodePosition position) {
Node* array_length = gasm_->LoadWasmArrayLength(array);
Node* range_end = gasm_->Int32Add(index, length);
Node* range_valid = gasm_->Word32And(
gasm_->Uint32LessThanOrEqual(range_end, array_length),
gasm_->Uint32LessThanOrEqual(index, range_end)); // No overflow
TrapIfFalse(wasm::kTrapArrayOutOfBounds, range_valid, position);
}
Node* WasmGraphBuilder::ArrayGet(Node* array_object,
const wasm::ArrayType* type, Node* index,
CheckForNull null_check, bool is_signed,
......@@ -5918,7 +5929,7 @@ Node* WasmGraphBuilder::ArrayGet(Node* array_object,
TrapIfTrue(wasm::kTrapNullDereference,
gasm_->WordEqual(array_object, RefNull()), position);
}
BoundsCheck(array_object, index, position);
BoundsCheckArray(array_object, index, position);
MachineType machine_type = MachineType::TypeForRepresentation(
type->element_type().machine_representation(), is_signed);
Node* offset = gasm_->WasmArrayElementOffset(index, type->element_type());
......@@ -5933,7 +5944,7 @@ void WasmGraphBuilder::ArraySet(Node* array_object, const wasm::ArrayType* type,
TrapIfTrue(wasm::kTrapNullDereference,
gasm_->WordEqual(array_object, RefNull()), position);
}
BoundsCheck(array_object, index, position);
BoundsCheckArray(array_object, index, position);
Node* offset = gasm_->WasmArrayElementOffset(index, type->element_type());
gasm_->StoreToObject(ObjectAccessForGCStores(type->element_type()),
array_object, offset, value);
......@@ -5948,6 +5959,23 @@ Node* WasmGraphBuilder::ArrayLen(Node* array_object, CheckForNull null_check,
return gasm_->LoadWasmArrayLength(array_object);
}
void WasmGraphBuilder::ArrayCopy(Node* dst_array, Node* dst_index,
Node* src_array, Node* src_index, Node* length,
wasm::WasmCodePosition position) {
// TODO(7748): Skip null checks when possible.
TrapIfTrue(wasm::kTrapNullDereference, gasm_->WordEqual(dst_array, RefNull()),
position);
TrapIfTrue(wasm::kTrapNullDereference, gasm_->WordEqual(src_array, RefNull()),
position);
BoundsCheckArrayCopy(dst_array, dst_index, length, position);
BoundsCheckArrayCopy(src_array, src_index, length, position);
Operator::Properties copy_properties =
Operator::kIdempotent | Operator::kNoThrow | Operator::kNoDeopt;
// The builtin needs the int parameters first.
gasm_->CallBuiltin(Builtins::kWasmArrayCopy, copy_properties, dst_index,
src_index, length, dst_array, src_array);
}
// 1 bit V8 Smi tag, 31 bits V8 Smi shift, 1 bit i31ref high-bit truncation.
constexpr int kI31To32BitSmiShift = 33;
......
......@@ -464,7 +464,6 @@ class WasmGraphBuilder {
Node* ArrayNewWithRtt(uint32_t array_index, const wasm::ArrayType* type,
Node* length, Node* initial_value, Node* rtt,
wasm::WasmCodePosition position);
void BoundsCheck(Node* array, Node* index, wasm::WasmCodePosition position);
Node* ArrayGet(Node* array_object, const wasm::ArrayType* type, Node* index,
CheckForNull null_check, bool is_signed,
wasm::WasmCodePosition position);
......@@ -473,6 +472,9 @@ class WasmGraphBuilder {
wasm::WasmCodePosition position);
Node* ArrayLen(Node* array_object, CheckForNull null_check,
wasm::WasmCodePosition position);
void ArrayCopy(Node* dst_array, Node* dst_index, Node* src_array,
Node* src_index, Node* length,
wasm::WasmCodePosition position);
Node* I31New(Node* input);
Node* I31GetS(Node* input);
Node* I31GetU(Node* input);
......@@ -684,6 +686,10 @@ class WasmGraphBuilder {
void BrOnCastAbs(Node** match_control, Node** match_effect,
Node** no_match_control, Node** no_match_effect,
std::function<void(Callbacks)> type_checker);
void BoundsCheckArray(Node* array, Node* index,
wasm::WasmCodePosition position);
void BoundsCheckArrayCopy(Node* array, Node* index, Node* length,
wasm::WasmCodePosition position);
// Asm.js specific functionality.
Node* BuildI32AsmjsSConvertF32(Node* input);
......
......@@ -84,7 +84,8 @@ class ErrorUtils : public AllStatic {
Handle<Object> message, Handle<Object> options, FrameSkipMode mode,
Handle<Object> caller, StackTraceCollection stack_trace_collection);
static MaybeHandle<String> ToString(Isolate* isolate, Handle<Object> recv);
V8_EXPORT_PRIVATE static MaybeHandle<String> ToString(Isolate* isolate,
Handle<Object> recv);
static Handle<JSObject> MakeGenericError(
Isolate* isolate, Handle<JSFunction> constructor, MessageTemplate index,
......
......@@ -966,6 +966,7 @@ DEFINE_STRING(dump_wasm_module_path, nullptr,
FOREACH_WASM_FEATURE_FLAG(DECL_WASM_FLAG)
#undef DECL_WASM_FLAG
DEFINE_IMPLICATION(experimental_wasm_gc_experiments, experimental_wasm_gc)
DEFINE_IMPLICATION(experimental_wasm_gc, experimental_wasm_typed_funcref)
DEFINE_IMPLICATION(experimental_wasm_typed_funcref, experimental_wasm_reftypes)
......
......@@ -637,5 +637,51 @@ RUNTIME_FUNCTION(Runtime_WasmAllocateRtt) {
return *wasm::AllocateSubRtt(isolate, instance, type_index, parent);
}
namespace {
inline void* ArrayElementAddress(Handle<WasmArray> array, uint32_t index,
int element_size_bytes) {
return reinterpret_cast<void*>(array->ptr() + WasmArray::kHeaderSize -
kHeapObjectTag + index * element_size_bytes);
}
} // namespace
// Assumes copy ranges are in-bounds.
RUNTIME_FUNCTION(Runtime_WasmArrayCopy) {
ClearThreadInWasmScope flag_scope(isolate);
HandleScope scope(isolate);
DCHECK_EQ(5, args.length());
CONVERT_ARG_HANDLE_CHECKED(WasmArray, dst_array, 0);
CONVERT_UINT32_ARG_CHECKED(dst_index, 1);
CONVERT_ARG_HANDLE_CHECKED(WasmArray, src_array, 2);
CONVERT_UINT32_ARG_CHECKED(src_index, 3);
CONVERT_UINT32_ARG_CHECKED(length, 4);
bool overlapping_ranges =
dst_array->ptr() == src_array->ptr() &&
(dst_index + length > src_index || src_index + length > dst_index);
wasm::ValueType element_type = src_array->type()->element_type();
if (element_type.is_reference()) {
ObjectSlot dst_slot = dst_array->ElementSlot(dst_index);
ObjectSlot src_slot = src_array->ElementSlot(src_index);
if (overlapping_ranges) {
isolate->heap()->MoveRange(*dst_array, dst_slot, src_slot, length,
UPDATE_WRITE_BARRIER);
} else {
isolate->heap()->CopyRange(*dst_array, dst_slot, src_slot, length,
UPDATE_WRITE_BARRIER);
}
} else {
int element_size_bytes = element_type.element_size_bytes();
void* dst = ArrayElementAddress(dst_array, dst_index, element_size_bytes);
void* src = ArrayElementAddress(src_array, src_index, element_size_bytes);
size_t copy_size = length * element_size_bytes;
if (overlapping_ranges) {
MemMove(dst, src, copy_size);
} else {
MemCopy(dst, src, copy_size);
}
}
return ReadOnlyRoots(isolate).undefined_value();
}
} // namespace internal
} // namespace v8
......@@ -588,7 +588,8 @@ namespace internal {
F(WasmCompileWrapper, 2, 1) \
F(WasmTriggerTierUp, 1, 1) \
F(WasmDebugBreak, 0, 1) \
F(WasmAllocateRtt, 2, 1)
F(WasmAllocateRtt, 2, 1) \
F(WasmArrayCopy, 5, 1)
#define FOR_EACH_INTRINSIC_WASM_TEST(F, I) \
F(DeserializeWasmModule, 2, 1) \
......
......@@ -5015,6 +5015,22 @@ class LiftoffCompiler {
__ PushRegister(kI32, len);
}
void ArrayCopy(FullDecoder* decoder, const Value& dst, const Value& dst_index,
const Value& src, const Value& src_index,
const Value& length) {
CallRuntimeStub(WasmCode::kWasmArrayCopyWithChecks,
MakeSig::Params(kI32, kI32, kI32, kOptRef, kOptRef),
// Builtin parameter order:
// [dst_index, src_index, length, dst, src].
{__ cache_state()->stack_state.end()[-4],
__ cache_state()->stack_state.end()[-2],
__ cache_state()->stack_state.end()[-1],
__ cache_state()->stack_state.end()[-5],
__ cache_state()->stack_state.end()[-3]},
decoder->position());
__ cache_state()->stack_state.pop_back(5);
}
// 1 bit Smi tag, 31 bits Smi shift, 1 bit i31ref high-bit truncation.
constexpr static int kI31To32BitSmiShift = 33;
......
......@@ -49,14 +49,14 @@ struct WasmException;
return true; \
}())
#define CHECK_PROTOTYPE_OPCODE(feat) \
DCHECK(this->module_->origin == kWasmOrigin); \
if (!VALIDATE(this->enabled_.has_##feat())) { \
this->DecodeError( \
"Invalid opcode 0x%x (enable with --experimental-wasm-" #feat ")", \
opcode); \
return 0; \
} \
#define CHECK_PROTOTYPE_OPCODE(feat) \
DCHECK(this->module_->origin == kWasmOrigin); \
if (!VALIDATE(this->enabled_.has_##feat())) { \
this->DecodeError( \
"Invalid opcode 0x%02x (enable with --experimental-wasm-" #feat ")", \
opcode); \
return 0; \
} \
this->detected_->Add(kFeature_##feat);
#define ATOMIC_OP_LIST(V) \
......@@ -1134,6 +1134,8 @@ struct ControlBase : public PcForErrors<validate> {
const ArrayIndexImmediate<validate>& imm, const Value& index, \
const Value& value) \
F(ArrayLen, const Value& array_obj, Value* result) \
F(ArrayCopy, const Value& src, const Value& src_index, const Value& dst, \
const Value& dst_index, const Value& length) \
F(I31New, const Value& input, Value* result) \
F(I31GetS, const Value& input, Value* result) \
F(I31GetU, const Value& input, Value* result) \
......@@ -1935,6 +1937,11 @@ class WasmDecoder : public Decoder {
ArrayIndexImmediate<validate> imm(decoder, pc + length);
return length + imm.length;
}
case kExprArrayCopy: {
ArrayIndexImmediate<validate> src_imm(decoder, pc + length);
ArrayIndexImmediate<validate> dst_imm(decoder, pc + length);
return length + src_imm.length + dst_imm.length;
}
case kExprBrOnCast:
case kExprBrOnCastFail:
case kExprBrOnData:
......@@ -2122,6 +2129,8 @@ class WasmDecoder : public Decoder {
return {2, 1};
case kExprArraySet:
return {3, 0};
case kExprArrayCopy:
return {5, 0};
case kExprRttCanon:
return {0, 1};
case kExprArrayNewWithRtt:
......@@ -4232,6 +4241,35 @@ class WasmFullDecoder : public WasmDecoder<validate> {
Push(value);
return opcode_length + imm.length;
}
case kExprArrayCopy: {
CHECK_PROTOTYPE_OPCODE(gc_experiments);
ArrayIndexImmediate<validate> dst_imm(this, this->pc_ + opcode_length);
if (!this->Validate(this->pc_ + opcode_length, dst_imm)) return 0;
ArrayIndexImmediate<validate> src_imm(
this, this->pc_ + opcode_length + dst_imm.length);
if (!this->Validate(this->pc_ + opcode_length + dst_imm.length,
src_imm)) {
return 0;
}
if (!IsSubtypeOf(src_imm.array_type->element_type(),
dst_imm.array_type->element_type(), this->module_)) {
this->DecodeError(
"array.copy: source array's #%d element type is not a subtype of "
"destination array's #%d element type",
src_imm.index, dst_imm.index);
return 0;
}
// [dst, dst_index, src, src_index, length]
Value dst = Peek(4, 0, ValueType::Ref(dst_imm.index, kNullable));
Value dst_index = Peek(3, 1, kWasmI32);
Value src = Peek(2, 2, ValueType::Ref(src_imm.index, kNullable));
Value src_index = Peek(1, 3, kWasmI32);
Value length = Peek(0, 4, kWasmI32);
CALL_INTERFACE_IF_OK_AND_REACHABLE(ArrayCopy, dst, dst_index, src,
src_index, length);
Drop(5);
return opcode_length + dst_imm.length + src_imm.length;
}
case kExprI31New: {
Value input = Peek(0, 0, kWasmI32);
Value value = CreateValue(kWasmI31Ref);
......
......@@ -980,6 +980,13 @@ class WasmGraphBuildingInterface {
builder_->ArrayLen(array_obj.node, null_check, decoder->position());
}
void ArrayCopy(FullDecoder* decoder, const Value& dst, const Value& dst_index,
const Value& src, const Value& src_index,
const Value& length) {
builder_->ArrayCopy(dst.node, dst_index.node, src.node, src_index.node,
length.node, decoder->position());
}
void I31New(FullDecoder* decoder, const Value& input, Value* result) {
result->node = builder_->I31New(input.node);
}
......
......@@ -98,6 +98,8 @@ struct WasmModule;
IF_TSAN(V, TSANRelaxedStoreIgnoreFP) \
IF_TSAN(V, TSANRelaxedStoreSaveFP) \
V(WasmAllocateArrayWithRtt) \
V(WasmArrayCopy) \
V(WasmArrayCopyWithChecks) \
V(WasmAllocateRtt) \
V(WasmAllocateStructWithRtt) \
V(WasmSubtypeCheck) \
......
......@@ -24,6 +24,10 @@
/* V8 side owner: jkummerow */ \
V(gc, "garbage collection", false) \
\
/* Non-specified, V8-only experimental additions to the GC proposal */ \
/* V8 side owner: jkummerow */ \
V(gc_experiments, "garbage collection V8-only experimental features", false) \
\
/* Typed function references proposal. */ \
/* Official proposal: https://github.com/WebAssembly/function-references */ \
/* V8 side owner: manoskouk */ \
......
......@@ -1668,6 +1668,12 @@ wasm::WasmValue WasmArray::GetElement(uint32_t index) {
}
}
ObjectSlot WasmArray::ElementSlot(uint32_t index) {
DCHECK_LE(index, length());
DCHECK(type()->element_type().is_reference());
return RawField(kHeaderSize + kTaggedSize * index);
}
// static
Handle<WasmExceptionObject> WasmExceptionObject::New(
Isolate* isolate, const wasm::FunctionSig* sig,
......
......@@ -987,6 +987,9 @@ class WasmArray : public TorqueGeneratedWasmArray<WasmArray, WasmObject> {
inline wasm::ArrayType* type() const;
static inline wasm::ArrayType* GcSafeType(Map map);
// Get the {ObjectSlot} corresponding to the element at {index}. Requires that
// this is a reference array.
ObjectSlot ElementSlot(uint32_t index);
wasm::WasmValue GetElement(uint32_t index);
static inline int SizeFor(Map map, int length);
......
......@@ -393,8 +393,9 @@ constexpr const char* WasmOpcodes::OpcodeName(WasmOpcode opcode) {
CASE_OP(ArrayGet, "array.get")
CASE_OP(ArrayGetS, "array.get_s")
CASE_OP(ArrayGetU, "array.get_u")
CASE_OP(ArrayLen, "array.len")
CASE_OP(ArraySet, "array.set")
CASE_OP(ArrayLen, "array.len")
CASE_OP(ArrayCopy, "array.copy")
CASE_OP(I31New, "i31.new")
CASE_OP(I31GetS, "i31.get_s")
CASE_OP(I31GetU, "i31.get_u")
......
......@@ -649,40 +649,41 @@ bool V8_EXPORT_PRIVATE IsJSCompatibleSignature(const FunctionSig* sig,
V(I64AtomicCompareExchange16U, 0xfe4d, l_ill) \
V(I64AtomicCompareExchange32U, 0xfe4e, l_ill)
#define FOREACH_GC_OPCODE(V) \
V(StructNewWithRtt, 0xfb01, _) \
V(StructNewDefault, 0xfb02, _) \
V(StructGet, 0xfb03, _) \
V(StructGetS, 0xfb04, _) \
V(StructGetU, 0xfb05, _) \
V(StructSet, 0xfb06, _) \
V(ArrayNewWithRtt, 0xfb11, _) \
V(ArrayNewDefault, 0xfb12, _) \
V(ArrayGet, 0xfb13, _) \
V(ArrayGetS, 0xfb14, _) \
V(ArrayGetU, 0xfb15, _) \
V(ArraySet, 0xfb16, _) \
V(ArrayLen, 0xfb17, _) \
V(I31New, 0xfb20, _) \
V(I31GetS, 0xfb21, _) \
V(I31GetU, 0xfb22, _) \
V(RttCanon, 0xfb30, _) \
V(RttSub, 0xfb31, _) \
V(RefTest, 0xfb40, _) \
V(RefCast, 0xfb41, _) \
V(BrOnCast, 0xfb42, _) \
V(BrOnCastFail, 0xfb43, _) \
V(RefIsFunc, 0xfb50, _) \
V(RefIsData, 0xfb51, _) \
V(RefIsI31, 0xfb52, _) \
V(RefAsFunc, 0xfb58, _) \
V(RefAsData, 0xfb59, _) \
V(RefAsI31, 0xfb5a, _) \
V(BrOnFunc, 0xfb60, _) \
V(BrOnData, 0xfb61, _) \
V(BrOnI31, 0xfb62, _) \
V(BrOnNonFunc, 0xfb63, _) \
V(BrOnNonData, 0xfb64, _) \
#define FOREACH_GC_OPCODE(V) \
V(StructNewWithRtt, 0xfb01, _) \
V(StructNewDefault, 0xfb02, _) \
V(StructGet, 0xfb03, _) \
V(StructGetS, 0xfb04, _) \
V(StructGetU, 0xfb05, _) \
V(StructSet, 0xfb06, _) \
V(ArrayNewWithRtt, 0xfb11, _) \
V(ArrayNewDefault, 0xfb12, _) \
V(ArrayGet, 0xfb13, _) \
V(ArrayGetS, 0xfb14, _) \
V(ArrayGetU, 0xfb15, _) \
V(ArraySet, 0xfb16, _) \
V(ArrayLen, 0xfb17, _) \
V(ArrayCopy, 0xfb18, _) /* not standardized - V8 experimental */ \
V(I31New, 0xfb20, _) \
V(I31GetS, 0xfb21, _) \
V(I31GetU, 0xfb22, _) \
V(RttCanon, 0xfb30, _) \
V(RttSub, 0xfb31, _) \
V(RefTest, 0xfb40, _) \
V(RefCast, 0xfb41, _) \
V(BrOnCast, 0xfb42, _) \
V(BrOnCastFail, 0xfb43, _) \
V(RefIsFunc, 0xfb50, _) \
V(RefIsData, 0xfb51, _) \
V(RefIsI31, 0xfb52, _) \
V(RefAsFunc, 0xfb58, _) \
V(RefAsData, 0xfb59, _) \
V(RefAsI31, 0xfb5a, _) \
V(BrOnFunc, 0xfb60, _) \
V(BrOnData, 0xfb61, _) \
V(BrOnI31, 0xfb62, _) \
V(BrOnNonFunc, 0xfb63, _) \
V(BrOnNonData, 0xfb64, _) \
V(BrOnNonI31, 0xfb65, _)
#define FOREACH_ATOMIC_0_OPERAND_OPCODE(V) \
......
This diff is collapsed.
......@@ -555,6 +555,11 @@ inline WasmOpcode LoadStoreOpcodeOf(MachineType type, bool store) {
array, index, value, WASM_GC_OP(kExprArraySet), static_cast<byte>(typeidx)
#define WASM_ARRAY_LEN(typeidx, array) \
array, WASM_GC_OP(kExprArrayLen), static_cast<byte>(typeidx)
#define WASM_ARRAY_COPY(dst_idx, src_idx, dst_array, dst_index, src_array, \
src_index, length) \
dst_array, dst_index, src_array, src_index, length, \
WASM_GC_OP(kExprArrayCopy), static_cast<byte>(dst_idx), \
static_cast<byte>(src_idx)
#define WASM_RTT_WITH_DEPTH(depth, typeidx) \
kRttWithDepthCode, U32V_1(depth), U32V_1(typeidx)
......
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