No public description

PiperOrigin-RevId: 998094460
Change-Id: Ic3196b4f5d12ee42dc0a7f0755c2969188f3d59d
diff --git a/riscv/BUILD b/riscv/BUILD
index 7ddc997..5176adf 100644
--- a/riscv/BUILD
+++ b/riscv/BUILD
@@ -272,6 +272,7 @@
         ":riscv_fp_state",
         ":riscv_g",
         ":riscv_state",
+        ":riscv_zvt_state",
         "@abseil-cpp//absl/log",
         "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/strings",
@@ -1741,7 +1742,9 @@
         "@abseil-cpp//absl/log",
         "@abseil-cpp//absl/log:check",
         "@abseil-cpp//absl/memory",
+        "@abseil-cpp//absl/status",
         "@abseil-cpp//absl/status:statusor",
+        "@abseil-cpp//absl/strings",
         "@mpact-sim//mpact/sim/generic:core",
         "@mpact-sim//mpact/sim/generic:instruction",
     ],
@@ -1756,7 +1759,10 @@
         ":riscv_state",
         ":riscv_zvt_state",
         "@abseil-cpp//absl/base",
-        "@abseil-cpp//absl/log:check",
+        "@abseil-cpp//absl/log",
+        "@abseil-cpp//absl/status",
+        "@abseil-cpp//absl/status:statusor",
+        "@abseil-cpp//absl/strings",
         "@abseil-cpp//absl/types:span",
         "@mpact-sim//mpact/sim/generic:core",
         "@mpact-sim//mpact/sim/generic:instruction",
diff --git a/riscv/riscv_vector_memory_instructions.cc b/riscv/riscv_vector_memory_instructions.cc
index 5366199..cedc8df 100644
--- a/riscv/riscv_vector_memory_instructions.cc
+++ b/riscv/riscv_vector_memory_instructions.cc
@@ -25,6 +25,7 @@
 #include "riscv/riscv_register.h"
 #include "riscv/riscv_state.h"
 #include "riscv/riscv_vector_state.h"
+#include "riscv/riscv_zvt_state.h"
 
 namespace mpact {
 namespace sim {
@@ -160,6 +161,10 @@
 void Vsetvl(bool rd_zero, bool rs1_zero, const Instruction* inst) {
   auto* rv_state = static_cast<RiscVState*>(inst->state());
   auto* rv_vector = rv_state->rv_vector();
+  auto m_result = GetMatrixState(inst);
+  if (m_result.ok()) {
+    (*m_result)->ResetMtype();
+  }
   uint32_t vtype = GetInstructionSource<uint32_t>(inst, 1) & 0b1'1'111'111;
   // Get previous vtype.
   uint32_t prev_vtype = rv_vector->vtype();
diff --git a/riscv/riscv_zvt_instructions.cc b/riscv/riscv_zvt_instructions.cc
index 9a7965b..4dfc74b 100644
--- a/riscv/riscv_zvt_instructions.cc
+++ b/riscv/riscv_zvt_instructions.cc
@@ -19,7 +19,10 @@
 #include <cstring>
 
 #include "absl/base/casts.h"
-#include "absl/log/check.h"
+#include "absl/log/log.h"
+#include "absl/status/status.h"
+#include "absl/status/statusor.h"
+#include "absl/strings/str_cat.h"
 #include "absl/types/span.h"
 #include "mpact/sim/generic/data_buffer.h"
 #include "mpact/sim/generic/instruction.h"
@@ -73,18 +76,28 @@
   const int kEffectiveTileEdge = tile_element_width < 64
                                      ? matrix_state->tile_elements()
                                      : matrix_state->tile_elements() / 2;
-  CHECK(row >= 0 && row < kEffectiveTileEdge) << "row out of bounds";
-  CHECK(col >= 0 && col < kEffectiveTileEdge) << "col out of bounds";
+  if (row < 0 || row >= kEffectiveTileEdge || col < 0 ||
+      col >= kEffectiveTileEdge) {
+    return 0;
+  }
+  auto load_elem = [](auto status_or_val) -> uint64_t {
+    if (!status_or_val.ok()) {
+      LOG(ERROR) << status_or_val.status();
+      return 0;
+    }
+    return static_cast<uint64_t>(*status_or_val);
+  };
   switch (tile_element_width) {
     case 8:
-      return matrix_state->GetElem<uint8_t>(kTile, row, col, 8);
+      return load_elem(matrix_state->GetElem<uint8_t>(kTile, row, col, 8));
     case 16:
-      return matrix_state->GetElem<uint16_t>(kTile, row, col, 16);
+      return load_elem(matrix_state->GetElem<uint16_t>(kTile, row, col, 16));
     case 32:
-      return matrix_state->GetElem<uint32_t>(kTile, row, col, 32);
+      return load_elem(matrix_state->GetElem<uint32_t>(kTile, row, col, 32));
     case 64:
-      return matrix_state->GetElem<uint64_t>(kTile, row, col, 64);
+      return load_elem(matrix_state->GetElem<uint64_t>(kTile, row, col, 64));
     default:
+      LOG(ERROR) << "Invalid tile element width: " << tile_element_width;
       return 0;
   }
 }
@@ -94,24 +107,34 @@
   const int kEffectiveTileEdge = tile_element_width < 64
                                      ? matrix_state->tile_elements()
                                      : matrix_state->tile_elements() / 2;
-  CHECK(row >= 0 && row < kEffectiveTileEdge) << "row out of bounds";
-  CHECK(col >= 0 && col < kEffectiveTileEdge) << "col out of bounds";
+  if (row < 0 || row >= kEffectiveTileEdge || col < 0 ||
+      col >= kEffectiveTileEdge) {
+    return;
+  }
+  absl::Status status;
   switch (tile_element_width) {
     case 8:
-      matrix_state->SetElem<uint8_t>(kTile, row, col, 8,
-                                     static_cast<uint8_t>(value));
+      status = matrix_state->SetElem<uint8_t>(kTile, row, col, 8,
+                                              static_cast<uint8_t>(value));
       break;
     case 16:
-      matrix_state->SetElem<uint16_t>(kTile, row, col, 16,
-                                      static_cast<uint16_t>(value));
+      status = matrix_state->SetElem<uint16_t>(kTile, row, col, 16,
+                                               static_cast<uint16_t>(value));
       break;
     case 32:
-      matrix_state->SetElem<uint32_t>(kTile, row, col, 32,
-                                      static_cast<uint32_t>(value));
+      status = matrix_state->SetElem<uint32_t>(kTile, row, col, 32,
+                                               static_cast<uint32_t>(value));
       break;
     case 64:
-      matrix_state->SetElem<uint64_t>(kTile, row, col, 64, value);
+      status = matrix_state->SetElem<uint64_t>(kTile, row, col, 64, value);
       break;
+    default:
+      status = absl::InvalidArgumentError(
+          absl::StrCat("Invalid tile element width: ", tile_element_width));
+      break;
+  }
+  if (!status.ok()) {
+    LOG(ERROR) << status;
   }
 }
 
@@ -449,20 +472,36 @@
   for (int mm = 0; mm < kTileM; mm++) {
     for (int nn = 0; nn < kTileN; nn++) {
       if (kIsFp) {
-        float acc =
+        absl::StatusOr<float> acc_res =
             matrix_state->GetElem<float>(kDestTile, mm, nn, kTileElementWidth);
+        if (!acc_res.ok()) {
+          LOG(ERROR) << acc_res.status();
+          return;
+        }
+        float acc = *acc_res;
         for (int kk = 0; kk < kTileK; kk++) {
           acc += read_fp(a, a_spans, kk, mm) * read_fp(b, b_spans, kk, nn);
         }
-        matrix_state->SetElem<float>(kDestTile, mm, nn, kTileElementWidth, acc);
+        // SetElem cannot fail because GetElem with the same parameters
+        // succeeded above.
+        matrix_state->SetElem<float>(kDestTile, mm, nn, kTileElementWidth, acc)
+            .IgnoreError();
       } else {
-        int32_t acc = matrix_state->GetElem<int32_t>(kDestTile, mm, nn,
-                                                     kTileElementWidth);
+        absl::StatusOr<int32_t> acc_res = matrix_state->GetElem<int32_t>(
+            kDestTile, mm, nn, kTileElementWidth);
+        if (!acc_res.ok()) {
+          LOG(ERROR) << acc_res.status();
+          return;
+        }
+        int32_t acc = *acc_res;
         for (int kk = 0; kk < kTileK; kk++) {
           acc += read_int(a, a_spans, kk, mm) * read_int(b, b_spans, kk, nn);
         }
-        matrix_state->SetElem<int32_t>(kDestTile, mm, nn, kTileElementWidth,
-                                       acc);
+        // SetElem cannot fail because GetElem with the same parameters
+        // succeeded above.
+        matrix_state
+            ->SetElem<int32_t>(kDestTile, mm, nn, kTileElementWidth, acc)
+            .IgnoreError();
       }
     }
   }
diff --git a/riscv/riscv_zvt_state.cc b/riscv/riscv_zvt_state.cc
index a00168d..d1327a0 100644
--- a/riscv/riscv_zvt_state.cc
+++ b/riscv/riscv_zvt_state.cc
@@ -21,21 +21,73 @@
 #include "absl/log/check.h"
 #include "absl/log/log.h"
 #include "absl/memory/memory.h"
+#include "absl/status/status.h"
+#include "absl/strings/str_cat.h"
 #include "mpact/sim/generic/data_buffer.h"
 #include "mpact/sim/generic/instruction.h"
 #include "riscv/riscv_csr.h"
 #include "riscv/riscv_state.h"
 #include "riscv/riscv_vector_state.h"
+#include "riscv/riscv_xstatus.h"
 
 namespace mpact::sim::riscv {
 
-uint32_t RiscVMtypeCsr::AsUint32() { return matrix_state_->mtype(); }
-uint64_t RiscVMtypeCsr::AsUint64() { return matrix_state_->mtype(); }
+namespace {
+constexpr uint32_t kMstatusVsMask = 0x0600U;
+}  // namespace
+
+uint32_t RiscVMtypeCsr::AsUint32() {
+  auto* state = matrix_state_->riscv_state();
+  if ((state->mstatus()->GetUint32() & kMstatusVsMask) == 0) {
+    state->Trap(
+        /*is_interrupt=*/false, 0,
+        static_cast<uint64_t>(
+            ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction),
+        0, nullptr);
+    return 0;
+  }
+  return matrix_state_->mtype();
+}
+uint64_t RiscVMtypeCsr::AsUint64() {
+  auto* state = matrix_state_->riscv_state();
+  if ((state->mstatus()->GetUint32() & kMstatusVsMask) == 0) {
+    state->Trap(
+        /*is_interrupt=*/false, 0,
+        static_cast<uint64_t>(
+            ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction),
+        0, nullptr);
+    return 0;
+  }
+  return matrix_state_->mtype();
+}
+
+void RiscVMtypeCsr::Write(uint32_t /*value*/) {
+  matrix_state_->riscv_state()->Trap(
+      /*is_interrupt=*/false, 0,
+      static_cast<uint64_t>(
+          ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction),
+      0, nullptr);
+}
+
+void RiscVMtypeCsr::Write(uint64_t /*value*/) {
+  matrix_state_->riscv_state()->Trap(
+      /*is_interrupt=*/false, 0,
+      static_cast<uint64_t>(
+          ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction),
+      0, nullptr);
+}
 
 absl::StatusOr<std::unique_ptr<RiscVZvtMatrixState>>
 RiscVZvtMatrixState::Create(
     ::mpact::sim::riscv::RiscVState* /* absl_nonnull */ state,
     int tile_elements) {
+  if (tile_elements < 4) {
+    return absl::InvalidArgumentError("Tile element width must be >= 4");
+  }
+  if ((tile_elements & (tile_elements - 1)) != 0) {
+    return absl::InvalidArgumentError(
+        absl::StrCat("TE must be a power of two: ", tile_elements));
+  }
   auto matrix = absl::WrapUnique(new RiscVZvtMatrixState(state, tile_elements));
   auto status = state->csr_set()->AddCsr(&matrix->mtype_csr_);
   if (!status.ok()) return status;
@@ -47,29 +99,21 @@
     int tile_elements)
     : state_(state), tile_elements_(tile_elements), mtype_csr_(this, state) {
   // TE must be a power of two in [4, VLEN/4]. VLEN/4 = 32 for VLEN=128b.
-  CHECK_GE(tile_elements_, 4) << "Tile element width must be >= 4";
-  CHECK_EQ(tile_elements_ & (tile_elements_ - 1), 0)
-      << "TE must be a power of two: " << tile_elements_;
-  tile_buffer_ = state_->db_factory()->Allocate<uint8_t>(16 * tile_elements *
-                                                         tile_elements);
+  tile_buffer_.reset(state_->db_factory()->Allocate<uint8_t>(
+      16 * tile_elements * tile_elements));
   for (auto& byte : tile_buffer_->Get<uint8_t>()) {
     byte = 0;
   }
 }
 
-RiscVZvtMatrixState::~RiscVZvtMatrixState() {
-  if (tile_buffer_ != nullptr) tile_buffer_->DecRef();
-  for (auto* db : scratch_dbs_) {
-    if (db != nullptr) db->DecRef();
-  }
-}
+RiscVZvtMatrixState::~RiscVZvtMatrixState() = default;
 
 ::mpact::sim::generic::DataBuffer* RiscVZvtMatrixState::GetScratchDb(int size) {
-  if (size >= scratch_dbs_.size()) scratch_dbs_.resize(size + 1, nullptr);
+  if (size >= scratch_dbs_.size()) scratch_dbs_.resize(size + 1);
   if (!scratch_dbs_[size]) {
-    scratch_dbs_[size] = state_->db_factory()->Allocate<uint8_t>(size);
+    scratch_dbs_[size].reset(state_->db_factory()->Allocate<uint8_t>(size));
   }
-  return scratch_dbs_[size];
+  return scratch_dbs_[size].get();
 }
 
 int RiscVZvtMatrixState::sew_bytes() const {
@@ -79,9 +123,9 @@
   return rv_vector == nullptr ? 1 : rv_vector->selected_element_width();
 }
 
-// Transcribed verbatim from the Zvt v0.3 spec (15.1.1.1, "Tile Punning").
-int RiscVZvtMatrixState::Punning(int tile, int row, int col,
-                                 int tile_element_width) const {
+// Transcribed verbatim from the Zvt v0.4 spec ("Tile Punning").
+absl::StatusOr<int> RiscVZvtMatrixState::GetPunningOffset(
+    int tile, int row, int col, int tile_element_width) const {
   const int kTileElements = tile_elements_;
   int ptile, minor_offset, major_offset;
   switch (tile_element_width) {
@@ -112,12 +156,35 @@
       break;
     }
     default:
-      LOG(FATAL) << "Invalid tile element width: " << tile_element_width;
+      return absl::InvalidArgumentError(
+          absl::StrCat("Invalid tile element width: ", tile_element_width));
   }
   return (ptile * kTileElements * kTileElements) + (major_offset * 16) +
          minor_offset;
 }
 
+namespace {
+int GetTileAlignment(int tile_element_width_bits) {
+  int ntiles = 16;
+  if (tile_element_width_bits == 16) {
+    ntiles = 8;
+  } else if (tile_element_width_bits == 32) {
+    ntiles = 4;
+  } else if (tile_element_width_bits == 64) {
+    ntiles = 8;
+  }
+  return 16 / ntiles;
+}
+}  // namespace
+
+bool RiscVZvtMatrixState::IsValidTile(int tile) const {
+  return (tile % GetTileAlignment(tile_element_width_bits())) == 0;
+}
+
+int RiscVZvtMatrixState::MaskTssTile(int tile, int tile_element_width) const {
+  return tile & ~(GetTileAlignment(tile_element_width) - 1);
+}
+
 absl::StatusOr<RiscVZvtMatrixState*> GetMatrixState(
     const ::mpact::sim::generic::Instruction* inst) {
   auto* rv_state = static_cast<::mpact::sim::riscv::RiscVState*>(inst->state());
diff --git a/riscv/riscv_zvt_state.h b/riscv/riscv_zvt_state.h
index 40a1007..a82a1a5 100644
--- a/riscv/riscv_zvt_state.h
+++ b/riscv/riscv_zvt_state.h
@@ -13,13 +13,13 @@
 // limitations under the License.
 
 // Architectural state for the RISC-V Vector Matrix Extension (Zvt / "VME",
-// spec v0.3, chapter 15.1). This is layered on top of the existing RISC-V
+// spec v0.4). This is layered on top of the existing RISC-V
 // vector state: the matrix unit sources its A/B operands from vector registers
 // and accumulates results into the new square matrix "tile" state modeled here.
 //
-// NOTE: Zvt is a draft (v0.3) extension. The mtype CSR field positions below
-// follow the v0.3 manual (15.1.1.2). They are isolated as named constants so
-// they can be adjusted if the encoding firms up.
+// NOTE: mtype CSR field positions below follow the v0.4 manual.
+// They are isolated as named constants so they can be adjusted if the
+// encoding firms up.
 
 #ifndef THIRD_PARTY_MPACT_RISCV_RISCV_ZVT_STATE_H_
 #define THIRD_PARTY_MPACT_RISCV_RISCV_ZVT_STATE_H_
@@ -30,6 +30,7 @@
 #include <vector>
 
 #include "absl/base/nullability.h"
+#include "absl/status/status.h"
 #include "absl/status/statusor.h"
 #include "mpact/sim/generic/data_buffer.h"
 #include "mpact/sim/generic/instruction.h"
@@ -81,6 +82,10 @@
   uint32_t AsUint32() override;
   uint64_t AsUint64() override;
 
+  // Software writes to mtype are illegal; they must trap.
+  void Write(uint32_t value) override;
+  void Write(uint64_t value) override;
+
   [[nodiscard]] RiscVZvtMatrixState* matrix_state() const {
     return matrix_state_;
   }
@@ -90,8 +95,17 @@
 };
 
 // Matrix tile state. Owns the flat tile buffer (16 * TE * TE bytes) and the
-// mtype CSR. All element access goes through Punning()/GetElem()/SetElem() so
-// the tile-aliasing ("punning") layout is implemented exactly once.
+// mtype CSR. All element access goes through GetPunningOffset()/GetElem()/
+// SetElem() so the tile-aliasing ("punning") layout is implemented exactly
+// once.
+struct DataBufferDeleter {
+  void operator()(::mpact::sim::generic::DataBuffer* db) const {
+    if (db) db->DecRef();
+  }
+};
+using SafeDataBuffer =
+    std::unique_ptr<::mpact::sim::generic::DataBuffer, DataBufferDeleter>;
+
 class RiscVZvtMatrixState {
  public:
   static absl::StatusOr<std::unique_ptr<RiscVZvtMatrixState>> Create(
@@ -112,30 +126,44 @@
 
   // Byte offset into tile_buffer_ for element (tile,row,col) at the given tile
   // element width `tile_element_width` (in bits: 8/16/32/64). Transcribed
-  // verbatim from the Zvt v0.3 spec (15.1.1.1, "Tile Punning"). `tile` is the
+  // verbatim from the Zvt v0.4 spec ("Tile Punning"). `tile` is the
   // named tile index (0..15); `tile_element_width` selects the aliasing scheme.
-  int Punning(int tile, int row, int col, int tile_element_width) const;
+  absl::StatusOr<int> GetPunningOffset(int tile, int row, int col,
+                                       int tile_element_width) const;
 
   template <typename T>
-  T GetElem(int tile, int row, int col, int tile_element_width) const {
+  absl::StatusOr<T> GetElem(int tile, int row, int col,
+                            int tile_element_width) const {
     T value;
     auto span = tile_buffer_->Get<uint8_t>();
-    std::memcpy(&value, &span[Punning(tile, row, col, tile_element_width)],
-                sizeof(T));
+    absl::StatusOr<int> offset =
+        GetPunningOffset(tile, row, col, tile_element_width);
+    if (!offset.ok()) return offset.status();
+    std::memcpy(&value, &span[*offset], sizeof(T));
     return value;
   }
 
   template <typename T>
-  void SetElem(int tile, int row, int col, int tile_element_width, T value) {
+  absl::Status SetElem(int tile, int row, int col, int tile_element_width,
+                       T value) {
     auto span = tile_buffer_->Get<uint8_t>();
-    std::memcpy(&span[Punning(tile, row, col, tile_element_width)], &value,
-                sizeof(T));
+    absl::StatusOr<int> offset =
+        GetPunningOffset(tile, row, col, tile_element_width);
+    if (!offset.ok()) return offset.status();
+    std::memcpy(&span[*offset], &value, sizeof(T));
+    return absl::OkStatus();
   }
 
   // mtype accessors. The mtype value is held here; the CSR is a read-only view.
   [[nodiscard]] uint32_t mtype() const { return mtype_; }
   void set_mtype(uint32_t value) { mtype_ = value; }
 
+  void ResetMtype() { set_mtype(0); }
+
+  // Tile index validation and masking (Zvt v0.4).
+  [[nodiscard]] bool IsValidTile(int tile) const;
+  [[nodiscard]] int MaskTssTile(int tile, int tile_element_width) const;
+
   // Decoded mtype fields.
   [[nodiscard]] int mtwiden() const {
     return (mtype() >> kMtypeMtwidenShift) & kMtypeMtwidenMask;
@@ -188,8 +216,8 @@
 
   ::mpact::sim::riscv::RiscVState* state_;
   int tile_elements_;
-  ::mpact::sim::generic::DataBuffer* tile_buffer_;  // 16 * TE * TE bytes.
-  std::vector<::mpact::sim::generic::DataBuffer*> scratch_dbs_;
+  SafeDataBuffer tile_buffer_;  // 16 * TE * TE bytes.
+  std::vector<SafeDataBuffer> scratch_dbs_;
   uint32_t mtype_ = 0;
   RiscVMtypeCsr mtype_csr_;
 };
diff --git a/riscv/test/BUILD b/riscv/test/BUILD
index 749e73e..9a65dba 100644
--- a/riscv/test/BUILD
+++ b/riscv/test/BUILD
@@ -1026,6 +1026,22 @@
     ],
 )
 
+cc_test(
+    name = "riscv_zvt_state_test",
+    size = "small",
+    srcs = ["riscv_zvt_state_test.cc"],
+    deps = [
+        "//riscv:riscv_state",
+        "//riscv:riscv_v",
+        "//riscv:riscv_zvt_state",
+        "@abseil-cpp//absl/status",
+        "@com_google_googletest//:gtest_main",
+        "@mpact-sim//mpact/sim/generic:core",
+        "@mpact-sim//mpact/sim/generic:instruction",
+        "@mpact-sim//mpact/sim/util/memory",
+    ],
+)
+
 config_setting(
     name = "arm_cpu",
     values = {"cpu": "arm"},
diff --git a/riscv/test/riscv_zvt_state_test.cc b/riscv/test/riscv_zvt_state_test.cc
new file mode 100644
index 0000000..dc71a23
--- /dev/null
+++ b/riscv/test/riscv_zvt_state_test.cc
@@ -0,0 +1,728 @@
+// Copyright 2026 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "riscv/riscv_zvt_state.h"
+
+#include <cstdint>
+#include <memory>
+#include <utility>
+
+#include "absl/status/status.h"
+#include "googlemock/include/gmock/gmock.h"
+#include "mpact/sim/generic/immediate_operand.h"  // NOLINT(misc-include-cleaner)
+#include "mpact/sim/generic/instruction.h"
+#include "mpact/sim/util/memory/flat_demand_memory.h"
+#include "riscv/riscv_register.h"
+#include "riscv/riscv_state.h"
+#include "riscv/riscv_vector_memory_instructions.h"
+#include "riscv/riscv_vector_state.h"
+
+/* start - helper test macro */
+#define CONCAT_INNER(a, b) a##b
+#define CONCAT(a, b) CONCAT_INNER(a, b)
+#define ASSERT_OK(expression) ASSERT_TRUE((expression).ok())
+#define ASSERT_OK_AND_ASSIGN(lhs, rexpr)            \
+  auto CONCAT(status_or_line_, __LINE__) = (rexpr); \
+  ASSERT_OK(CONCAT(status_or_line_, __LINE__));     \
+  lhs = std::move(CONCAT(status_or_line_, __LINE__)).value();
+/* end - helper test macro */
+
+namespace {
+
+using ::mpact::sim::riscv::RiscVState;
+using ::mpact::sim::riscv::RiscVXlen;
+using ::mpact::sim::riscv::RiscVZvtMatrixState;
+using ::mpact::sim::util::FlatDemandMemory;
+
+TEST(RiscVZvtStateTest, MtypeLayoutConfiguration) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  // Test mtwiden (bits 0-1)
+  matrix_state->set_mtype(0x3);
+  EXPECT_EQ(matrix_state->mtwiden(), 3);
+  EXPECT_EQ(matrix_state->twiden(), 4);
+  EXPECT_TRUE(matrix_state->configured());
+
+  // Test unconfigured (mtwiden = 0)
+  matrix_state->set_mtype(0x0);
+  EXPECT_EQ(matrix_state->mtwiden(), 0);
+  EXPECT_EQ(matrix_state->twiden(), 0);
+  EXPECT_FALSE(matrix_state->configured());
+
+  // Test tk (bits 5-7)
+  matrix_state->set_mtype(0x7 << 5);
+  EXPECT_EQ(matrix_state->tile_k(), 7);
+
+  // Test tm (bits 10-23)
+  matrix_state->set_mtype(0x3FFF << 10);
+  EXPECT_EQ(matrix_state->tile_m(), 0x3FFF);
+}
+
+TEST(RiscVZvtStateTest, MtypeCsrWriteTrapsIllegalInstruction) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  state->mstatus()->Set((state->mstatus()->GetUint32() & ~0x0600U) | (1U << 9));
+
+  // Fetch CSR using the constant address.
+  ASSERT_OK_AND_ASSIGN(auto* csr, state->csr_set()->GetCsr(
+                                      mpact::sim::riscv::kRiscVMtypeCsrIndex));
+  auto* mtype_csr = csr;
+
+  // Set up the trap callback.
+  bool trap_taken = false;
+  uint64_t trap_code = 0;
+  state->set_on_trap([&](bool, uint64_t, uint64_t exception_code, uint64_t,
+                         const ::mpact::sim::generic::Instruction*) {
+    trap_taken = true;
+    trap_code = exception_code;
+    return true;
+  });
+
+  // Attempt to write via CSR interface. This must trap since mtype is
+  // read-only.
+  mtype_csr->Write(static_cast<uint32_t>(0xDEADBEEF));
+  EXPECT_TRUE(trap_taken);
+  EXPECT_EQ(trap_code,
+            static_cast<uint64_t>(
+                ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction));
+
+  trap_taken = false;
+  trap_code = 0;
+  mtype_csr->Write(static_cast<uint64_t>(0xDEADBEEF));
+  EXPECT_TRUE(trap_taken);
+  EXPECT_EQ(trap_code,
+            static_cast<uint64_t>(
+                ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction));
+}
+
+struct PunningOffsets {
+  int ptile;
+  int minor_offset;
+  int major_offset;
+  int expected_offset;
+};
+
+// Number of architectural tile-specifier steps between valid tiles for a given
+// `tile_element_width` (Zvt v0.4 Section "Matrix Tile State"):
+//   TEW=8  -> 16 tiles (step 1: mt0, mt1, ..., mt15)
+//   TEW=16 ->  8 tiles (step 2: mt0, mt2, ..., mt14)
+//   TEW=32 ->  4 tiles (step 4: mt0, mt4, mt8, mt12)
+//   TEW=64 ->  8 tiles (step 2: mt0, mt2, ..., mt14)
+int TileAlignmentForTew(int tile_element_width) {
+  switch (tile_element_width) {
+    case 8:
+      return 1;
+    case 16:
+    case 64:
+      return 2;
+    case 32:
+      return 4;
+    default:
+      return 1;
+  }
+}
+
+// Effective number of elements along a tile edge (ETE) as a function of
+// `tile_elements` (TE) and `tile_element_width` (TEW) (Zvt v0.4):
+//   ETE = (TEW < 64) ? TE : (TE / 2).
+int EffectiveTileEdge(int tile_elements, int tile_element_width) {
+  return tile_element_width < 64 ? tile_elements : tile_elements / 2;
+}
+
+// Computes the Zvt v0.4 Section "Tile Punning" decomposition:
+//   - `ptile`: physical TEW=8 tile index (0..15) in the 16*TE*TE-byte buffer
+//   - `minor_offset`: byte offset (0..15) within the 16-byte minor block
+//   - `major_offset`: index of the 16-byte minor block within `ptile`
+//   - `expected_offset`: linear byte offset in `tile_buffer_`
+PunningOffsets ComputeExpectedPunningOffsets(int tile_elements, int tile,
+                                             int row, int col,
+                                             int tile_element_width) {
+  constexpr int kMinorBlockBytes = 16;
+  constexpr int kSubblockEdge = 4;
+  const int kSubblocksPerEdge = tile_elements / kSubblockEdge;
+  int ptile = 0;
+  int minor_offset = 0;
+  int major_offset = 0;
+  switch (tile_element_width) {
+    case 8:
+      ptile = tile;
+      minor_offset =
+          (row % kSubblockEdge) * kSubblockEdge + (col % kSubblockEdge);
+      major_offset =
+          (row / kSubblockEdge) * kSubblocksPerEdge + (col / kSubblockEdge);
+      break;
+    case 16:
+      ptile = tile + ((row & 2) >> 1);
+      minor_offset = (row % 2) * 4 + (col % 2) * 2 + ((col / 2) % 2) * 8;
+      major_offset =
+          (row / kSubblockEdge) * kSubblocksPerEdge + (col / kSubblockEdge);
+      break;
+    case 32:
+      ptile = tile + 2 * (row / (tile_elements / 2)) + ((col & 2) >> 1);
+      minor_offset = (row % 2) * 8 + (col % 2) * 4;
+      major_offset = ((row / 2) % kSubblocksPerEdge) * kSubblocksPerEdge +
+                     (col / kSubblockEdge);
+      break;
+    case 64:
+      ptile = tile + (row / kSubblocksPerEdge);
+      minor_offset = (col % 2) * 8;
+      major_offset = (row % kSubblocksPerEdge) * kSubblocksPerEdge + (col / 2);
+      break;
+  }
+  int expected_offset = (ptile * tile_elements * tile_elements) +
+                        (major_offset * kMinorBlockBytes) + minor_offset;
+  return {ptile, minor_offset, major_offset, expected_offset};
+}
+
+TEST(RiscVZvtStateTest, PunningTEW8) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  constexpr int kTileElements = 32;
+  constexpr int kTileElementWidth = 8;
+  const int kTileAlignment = TileAlignmentForTew(kTileElementWidth);
+  const int kEffectiveTileEdge =
+      EffectiveTileEdge(kTileElements, kTileElementWidth);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), kTileElements));
+
+  // 1) Origin corner: first valid tile (0), row 0, col 0.
+  ASSERT_OK_AND_ASSIGN(
+      int offset0,
+      matrix_state->GetPunningOffset(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset0, 0);
+
+  // 2) Interior element: aligned tile specifier and interior (row, col) with
+  // non-zero quotients and remainders modulo 2 and 4.
+  const int kInteriorTile = 3 * kTileAlignment;
+  constexpr int kInteriorRow = 10;
+  constexpr int kInteriorCol = 7;
+  auto [ptile1, minor_offset1, major_offset1, expected_offset1] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kInteriorTile,
+                                    /*row=*/kInteriorRow,
+                                    /*col=*/kInteriorCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_LT(ptile1, 16);
+  EXPECT_LT(minor_offset1, 16);
+  EXPECT_LT(major_offset1, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset1,
+      matrix_state->GetPunningOffset(/*tile=*/kInteriorTile,
+                                     /*row=*/kInteriorRow, /*col=*/kInteriorCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset1, expected_offset1);
+
+  // 3) Upper-bound corner: last valid tile and last (row, col) within ETE.
+  const int kMaxValidTile = 16 - kTileAlignment;
+  const int kMaxRow = kEffectiveTileEdge - 1;
+  const int kMaxCol = kEffectiveTileEdge - 1;
+  auto [ptile2, minor_offset2, major_offset2, expected_offset2] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kMaxValidTile,
+                                    /*row=*/kMaxRow,
+                                    /*col=*/kMaxCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_EQ(ptile2, 15);
+  EXPECT_LT(minor_offset2, 16);
+  EXPECT_LT(major_offset2, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset2,
+      matrix_state->GetPunningOffset(/*tile=*/kMaxValidTile, /*row=*/kMaxRow,
+                                     /*col=*/kMaxCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset2, expected_offset2);
+  EXPECT_EQ(offset2,
+            matrix_state->tile_buffer_bytes() - (kTileElementWidth / 8));
+}
+
+TEST(RiscVZvtStateTest, PunningTEW16) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  constexpr int kTileElements = 32;
+  constexpr int kTileElementWidth = 16;
+  const int kTileAlignment = TileAlignmentForTew(kTileElementWidth);
+  const int kEffectiveTileEdge =
+      EffectiveTileEdge(kTileElements, kTileElementWidth);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), kTileElements));
+
+  // 1) Origin corner: first valid tile (0), row 0, col 0.
+  ASSERT_OK_AND_ASSIGN(
+      int offset0,
+      matrix_state->GetPunningOffset(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset0, 0);
+
+  // 2) Interior element: aligned tile specifier and interior (row, col).
+  const int kInteriorTile = 3 * kTileAlignment;
+  constexpr int kInteriorRow = 10;
+  constexpr int kInteriorCol = 7;
+  auto [ptile1, minor_offset1, major_offset1, expected_offset1] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kInteriorTile,
+                                    /*row=*/kInteriorRow,
+                                    /*col=*/kInteriorCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_LT(ptile1, 16);
+  EXPECT_LT(minor_offset1, 16);
+  EXPECT_LT(major_offset1, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset1,
+      matrix_state->GetPunningOffset(/*tile=*/kInteriorTile,
+                                     /*row=*/kInteriorRow, /*col=*/kInteriorCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset1, expected_offset1);
+
+  // 3) Upper-bound corner: last valid tile and last (row, col) within ETE.
+  const int kMaxValidTile = 16 - kTileAlignment;
+  const int kMaxRow = kEffectiveTileEdge - 1;
+  const int kMaxCol = kEffectiveTileEdge - 1;
+  auto [ptile2, minor_offset2, major_offset2, expected_offset2] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kMaxValidTile,
+                                    /*row=*/kMaxRow,
+                                    /*col=*/kMaxCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_EQ(ptile2, 15);
+  EXPECT_LT(minor_offset2, 16);
+  EXPECT_LT(major_offset2, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset2,
+      matrix_state->GetPunningOffset(/*tile=*/kMaxValidTile, /*row=*/kMaxRow,
+                                     /*col=*/kMaxCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset2, expected_offset2);
+  EXPECT_EQ(offset2,
+            matrix_state->tile_buffer_bytes() - (kTileElementWidth / 8));
+}
+
+TEST(RiscVZvtStateTest, PunningTEW32) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  constexpr int kTileElements = 32;
+  constexpr int kTileElementWidth = 32;
+  const int kTileAlignment = TileAlignmentForTew(kTileElementWidth);
+  const int kEffectiveTileEdge =
+      EffectiveTileEdge(kTileElements, kTileElementWidth);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), kTileElements));
+
+  // 1) Origin corner: first valid tile (0), row 0, col 0.
+  ASSERT_OK_AND_ASSIGN(
+      int offset0,
+      matrix_state->GetPunningOffset(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset0, 0);
+
+  // 2) Interior element: aligned tile specifier and interior (row, col).
+  const int kInteriorTile = 3 * kTileAlignment;
+  constexpr int kInteriorRow = 10;
+  constexpr int kInteriorCol = 7;
+  auto [ptile1, minor_offset1, major_offset1, expected_offset1] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kInteriorTile,
+                                    /*row=*/kInteriorRow,
+                                    /*col=*/kInteriorCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_LT(ptile1, 16);
+  EXPECT_LT(minor_offset1, 16);
+  EXPECT_LT(major_offset1, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset1,
+      matrix_state->GetPunningOffset(/*tile=*/kInteriorTile,
+                                     /*row=*/kInteriorRow, /*col=*/kInteriorCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset1, expected_offset1);
+
+  // 3) Upper-bound corner: last valid tile and last (row, col) within ETE.
+  const int kMaxValidTile = 16 - kTileAlignment;
+  const int kMaxRow = kEffectiveTileEdge - 1;
+  const int kMaxCol = kEffectiveTileEdge - 1;
+  auto [ptile2, minor_offset2, major_offset2, expected_offset2] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kMaxValidTile,
+                                    /*row=*/kMaxRow,
+                                    /*col=*/kMaxCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_EQ(ptile2, 15);
+  EXPECT_LT(minor_offset2, 16);
+  EXPECT_LT(major_offset2, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset2,
+      matrix_state->GetPunningOffset(/*tile=*/kMaxValidTile, /*row=*/kMaxRow,
+                                     /*col=*/kMaxCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset2, expected_offset2);
+  EXPECT_EQ(offset2,
+            matrix_state->tile_buffer_bytes() - (kTileElementWidth / 8));
+}
+
+TEST(RiscVZvtStateTest, PunningTEW64) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  constexpr int kTileElements = 32;
+  constexpr int kTileElementWidth = 64;
+  const int kTileAlignment = TileAlignmentForTew(kTileElementWidth);
+  const int kEffectiveTileEdge =
+      EffectiveTileEdge(kTileElements, kTileElementWidth);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), kTileElements));
+
+  // 1) Origin corner: first valid tile (0), row 0, col 0.
+  ASSERT_OK_AND_ASSIGN(
+      int offset0,
+      matrix_state->GetPunningOffset(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset0, 0);
+
+  // 2) Interior element: aligned tile specifier and interior (row, col).
+  const int kInteriorTile = 3 * kTileAlignment;
+  constexpr int kInteriorRow = 10;
+  constexpr int kInteriorCol = 7;
+  auto [ptile1, minor_offset1, major_offset1, expected_offset1] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kInteriorTile,
+                                    /*row=*/kInteriorRow,
+                                    /*col=*/kInteriorCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_LT(ptile1, 16);
+  EXPECT_LT(minor_offset1, 16);
+  EXPECT_LT(major_offset1, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset1,
+      matrix_state->GetPunningOffset(/*tile=*/kInteriorTile,
+                                     /*row=*/kInteriorRow, /*col=*/kInteriorCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset1, expected_offset1);
+
+  // 3) Upper-bound corner: last valid tile and last (row, col) within ETE.
+  const int kMaxValidTile = 16 - kTileAlignment;
+  const int kMaxRow = kEffectiveTileEdge - 1;
+  const int kMaxCol = kEffectiveTileEdge - 1;
+  auto [ptile2, minor_offset2, major_offset2, expected_offset2] =
+      ComputeExpectedPunningOffsets(/*tile_elements=*/kTileElements,
+                                    /*tile=*/kMaxValidTile,
+                                    /*row=*/kMaxRow,
+                                    /*col=*/kMaxCol,
+                                    /*tile_element_width=*/kTileElementWidth);
+  EXPECT_EQ(ptile2, 15);
+  EXPECT_LT(minor_offset2, 16);
+  EXPECT_LT(major_offset2, (kTileElements / 4) * (kTileElements / 4));
+  ASSERT_OK_AND_ASSIGN(
+      int offset2,
+      matrix_state->GetPunningOffset(/*tile=*/kMaxValidTile, /*row=*/kMaxRow,
+                                     /*col=*/kMaxCol,
+                                     /*tile_element_width=*/kTileElementWidth));
+  EXPECT_EQ(offset2, expected_offset2);
+  EXPECT_EQ(offset2,
+            matrix_state->tile_buffer_bytes() - (kTileElementWidth / 8));
+}
+
+TEST(RiscVZvtStateTest, TileSpecifierAlignmentAndMasking) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  // Without vector state, sew_bytes() defaults to 1 (SEW = 8 bits).
+  // tew = sew(8) * twiden.
+  // We manipulate twiden via mtwiden. twiden = 1 << (mtwiden - 1).
+
+  // TEW=8 (mtwiden=1 => twiden=1). NTiles = 16. align = 1.
+  matrix_state->set_mtype(1 << mpact::sim::riscv::kMtypeMtwidenShift);
+  EXPECT_TRUE(matrix_state->IsValidTile(15));
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/15, /*tile_element_width=*/8),
+            15);
+
+  // TEW=16 (mtwiden=2 => twiden=2). NTiles = 8. align = 2.
+  matrix_state->set_mtype(2 << mpact::sim::riscv::kMtypeMtwidenShift);
+  EXPECT_TRUE(matrix_state->IsValidTile(0));
+  EXPECT_TRUE(matrix_state->IsValidTile(2));
+  EXPECT_FALSE(matrix_state->IsValidTile(3));
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/3, /*tile_element_width=*/16),
+            2);
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/15, /*tile_element_width=*/16),
+            14);
+
+  // TEW=32 (mtwiden=3 => twiden=4). NTiles = 4. align = 4.
+  matrix_state->set_mtype(3 << mpact::sim::riscv::kMtypeMtwidenShift);
+  EXPECT_TRUE(matrix_state->IsValidTile(4));
+  EXPECT_FALSE(matrix_state->IsValidTile(5));
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/7, /*tile_element_width=*/32),
+            4);
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/15, /*tile_element_width=*/32),
+            12);
+}
+
+TEST(RiscVZvtStateTest, TileSpecifierAlignmentAndMasking64) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  auto vector_state =
+      std::make_unique<mpact::sim::riscv::RiscVVectorState>(state.get(), 16);
+  // vtype[5:3] = 1 (SEW=16 bits)
+  vector_state->SetVectorType(1 << 3);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  // TEW=64 (sew=16, mtwiden=3 => twiden=4). NTiles = 8. align = 2.
+  matrix_state->set_mtype(3 << mpact::sim::riscv::kMtypeMtwidenShift);
+  EXPECT_TRUE(matrix_state->IsValidTile(0));
+  EXPECT_TRUE(matrix_state->IsValidTile(2));
+  EXPECT_TRUE(matrix_state->IsValidTile(8));
+  EXPECT_FALSE(matrix_state->IsValidTile(9));
+  EXPECT_FALSE(matrix_state->IsValidTile(3));
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/15, /*tile_element_width=*/64),
+            14);
+  EXPECT_EQ(matrix_state->MaskTssTile(/*tile=*/7, /*tile_element_width=*/64),
+            6);
+}
+
+TEST(RiscVZvtStateTest, VsetvlZeroesMtype) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  state->mstatus()->Set((state->mstatus()->GetUint32() & ~0x0600U) | (1U << 9));
+  auto vector_state =
+      std::make_unique<mpact::sim::riscv::RiscVVectorState>(state.get(), 16);
+  vector_state->SetVectorType(0);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  matrix_state->set_mtype(0xFFFFFFFF);
+  mpact::sim::generic::Instruction inst(0, state.get());
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(32));
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(0));
+  auto* reg = state->GetRegister<mpact::sim::riscv::RV32Register>("x1").first;
+  inst.AppendDestination(reg->CreateDestinationOperand(0));
+
+  mpact::sim::riscv::Vsetvl(/*rd_zero=*/false, /*rs1_zero=*/false, &inst);
+  EXPECT_EQ(matrix_state->mtype(), 0);
+}
+
+TEST(RiscVZvtStateTest, VsetvliZeroesMtype) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  state->mstatus()->Set((state->mstatus()->GetUint32() & ~0x0600U) | (1U << 9));
+  auto vector_state =
+      std::make_unique<mpact::sim::riscv::RiscVVectorState>(state.get(), 16);
+  vector_state->SetVectorType(0);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  matrix_state->set_mtype(0xFFFFFFFF);
+  mpact::sim::generic::Instruction inst(0, state.get());
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(32));
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(0));
+  auto* reg = state->GetRegister<mpact::sim::riscv::RV32Register>("x1").first;
+  inst.AppendDestination(reg->CreateDestinationOperand(0));
+
+  mpact::sim::riscv::Vsetvl(/*rd_zero=*/false, /*rs1_zero=*/true, &inst);
+  EXPECT_EQ(matrix_state->mtype(), 0);
+}
+
+TEST(RiscVZvtStateTest, VsetivliZeroesMtype) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  state->mstatus()->Set((state->mstatus()->GetUint32() & ~0x0600U) | (1U << 9));
+  auto vector_state =
+      std::make_unique<mpact::sim::riscv::RiscVVectorState>(state.get(), 16);
+  vector_state->SetVectorType(0);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  matrix_state->set_mtype(0xFFFFFFFF);
+  mpact::sim::generic::Instruction inst(0, state.get());
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(32));
+  inst.AppendSource(new mpact::sim::generic::ImmediateOperand<uint32_t>(0));
+  auto* reg = state->GetRegister<mpact::sim::riscv::RV32Register>("x0").first;
+  inst.AppendDestination(reg->CreateDestinationOperand(0));
+
+  mpact::sim::riscv::Vsetvl(/*rd_zero=*/true, /*rs1_zero=*/true, &inst);
+  EXPECT_EQ(matrix_state->mtype(), 0);
+}
+
+TEST(RiscVZvtStateTest, MtypeCsrTrapWhenVsOff) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  bool trap_taken = false;
+  uint64_t trap_code = 0;
+  state->set_on_trap([&trap_taken, &trap_code](
+                         bool, uint64_t, uint64_t exception_code, uint64_t,
+                         const mpact::sim::generic::Instruction*) {
+    trap_taken = true;
+    trap_code = exception_code;
+    return true;
+  });
+
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  auto* mstatus = state->mstatus();
+  mstatus->Set(mstatus->GetUint32() & ~0x0600U);  // VS bits [10:9] = 0 (Off)
+
+  ASSERT_OK_AND_ASSIGN(
+      auto* mtype,
+      state->csr_set()->GetCsr(mpact::sim::riscv::kRiscVMtypeCsrIndex));
+
+  trap_taken = false;
+  mtype->AsUint32();
+  EXPECT_TRUE(trap_taken);
+  EXPECT_EQ(trap_code,
+            static_cast<uint64_t>(
+                ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction));
+
+  trap_taken = false;
+  mtype->AsUint64();
+  EXPECT_TRUE(trap_taken);
+  EXPECT_EQ(trap_code,
+            static_cast<uint64_t>(
+                ::mpact::sim::riscv::ExceptionCode::kIllegalInstruction));
+}
+
+TEST(RiscVZvtStateTest, MtypeCsrSucceedsWhenMsOffAndVsOn) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  bool trap_taken = false;
+  state->set_on_trap([&trap_taken](bool, uint64_t, uint64_t, uint64_t,
+                                   const mpact::sim::generic::Instruction*) {
+    trap_taken = true;
+    return true;
+  });
+
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  auto* mstatus = state->mstatus();
+  // VS bits [10:9] = 1 (Initial), MS bits [30:29] = 0 (Off).
+  mstatus->Set((mstatus->GetUint32() & ~0x6000'0600U) | (1U << 9));
+
+  ASSERT_OK_AND_ASSIGN(
+      auto* mtype,
+      state->csr_set()->GetCsr(mpact::sim::riscv::kRiscVMtypeCsrIndex));
+
+  trap_taken = false;
+  mtype->AsUint32();
+  EXPECT_FALSE(trap_taken);
+
+  mtype->AsUint64();
+  EXPECT_FALSE(trap_taken);
+}
+
+TEST(RiscVZvtStateTest, CreateValidatesTileElements) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  auto matrix_state_res = RiscVZvtMatrixState::Create(state.get(), 0);
+  EXPECT_FALSE(matrix_state_res.ok());
+  EXPECT_EQ(matrix_state_res.status().code(),
+            absl::StatusCode::kInvalidArgument);
+
+  auto matrix_state_neg_res = RiscVZvtMatrixState::Create(state.get(), -4);
+  EXPECT_FALSE(matrix_state_neg_res.ok());
+  EXPECT_EQ(matrix_state_neg_res.status().code(),
+            absl::StatusCode::kInvalidArgument);
+
+  // Non-power-of-two tile_elements >= 4 must be rejected.
+  auto matrix_state_non_pow2_res = RiscVZvtMatrixState::Create(state.get(), 6);
+  EXPECT_FALSE(matrix_state_non_pow2_res.ok());
+  EXPECT_EQ(matrix_state_non_pow2_res.status().code(),
+            absl::StatusCode::kInvalidArgument);
+
+  // Creating a second matrix state on the same RiscVState fails because the
+  // mtype CSR is already registered.
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+  auto duplicate_res = RiscVZvtMatrixState::Create(state.get(), 32);
+  EXPECT_FALSE(duplicate_res.ok());
+}
+
+TEST(RiscVZvtStateTest, GetScratchDbAllocatesAndCaches) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  // Allocate a scratch DataBuffer of size 16.
+  auto* db16 = matrix_state->GetScratchDb(16);
+  ASSERT_NE(db16, nullptr);
+  EXPECT_EQ(db16->size<uint8_t>(), 16);
+
+  // Requesting the same size again returns the cached DataBuffer.
+  EXPECT_EQ(matrix_state->GetScratchDb(16), db16);
+
+  // Requesting a smaller size that has not been allocated yet allocates it
+  // without resizing scratch_dbs_.
+  auto* db8 = matrix_state->GetScratchDb(8);
+  ASSERT_NE(db8, nullptr);
+  EXPECT_EQ(db8->size<uint8_t>(), 8);
+  EXPECT_NE(db8, db16);
+}
+
+TEST(RiscVZvtStateTest, GetAndSetElemAndInvalidWidthErrorPaths) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+
+  // Invalid tile element width in GetPunningOffset, SetElem, and GetElem.
+  EXPECT_EQ(matrix_state
+                ->GetPunningOffset(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                   /*tile_element_width=*/12)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(matrix_state
+                ->SetElem<uint32_t>(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                    /*tile_element_width=*/12, /*val=*/42U)
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+  EXPECT_EQ(matrix_state
+                ->GetElem<uint32_t>(/*tile=*/0, /*row=*/0, /*col=*/0,
+                                    /*tile_element_width=*/12)
+                .status()
+                .code(),
+            absl::StatusCode::kInvalidArgument);
+
+  // Valid SetElem and GetElem round-trip.
+  ASSERT_OK(matrix_state->SetElem<uint32_t>(
+      /*tile=*/0, /*row=*/1, /*col=*/2, /*tile_element_width=*/32,
+      /*val=*/0xCAFEBABEU));
+  ASSERT_OK_AND_ASSIGN(uint32_t val, matrix_state->GetElem<uint32_t>(
+                                         /*tile=*/0, /*row=*/1, /*col=*/2,
+                                         /*tile_element_width=*/32));
+  EXPECT_EQ(val, 0xCAFEBABEU);
+}
+
+TEST(RiscVZvtStateTest, GetMatrixStateLookup) {
+  FlatDemandMemory memory;
+  auto state = std::make_unique<RiscVState>("test", RiscVXlen::RV32, &memory);
+  mpact::sim::generic::Instruction inst(0, state.get());
+
+  // Before RiscVZvtMatrixState::Create, mtype CSR is not registered.
+  auto missing_res = mpact::sim::riscv::GetMatrixState(&inst);
+  EXPECT_FALSE(missing_res.ok());
+
+  ASSERT_OK_AND_ASSIGN(auto matrix_state,
+                       RiscVZvtMatrixState::Create(state.get(), 32));
+  ASSERT_OK_AND_ASSIGN(auto* found_state,
+                       mpact::sim::riscv::GetMatrixState(&inst));
+  EXPECT_EQ(found_state, matrix_state.get());
+}
+
+}  // namespace