[magma] Add magma_poll

magma_poll allows the client to handle notification messages while
waiting for semaphores.

Intended to replace magma_wait_semaphores and magma_wait_notification_channel.

Bug:49117

Testability:new tests

Test:
acer:go/magma-tps#L1

Change-Id: I74b5f303475de756bcd1a80bb075fa4f522b8255
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/375421
Commit-Queue: Craig Stout <cstout@google.com>
Reviewed-by: John Bauman <jbauman@google.com>
Testability-Review: John Bauman <jbauman@google.com>
diff --git a/src/graphics/drivers/msd-arm-mali/tests/unit_tests/test_device.cc b/src/graphics/drivers/msd-arm-mali/tests/unit_tests/test_device.cc
index 39bd2f6..5f7ee19 100644
--- a/src/graphics/drivers/msd-arm-mali/tests/unit_tests/test_device.cc
+++ b/src/graphics/drivers/msd-arm-mali/tests/unit_tests/test_device.cc
@@ -177,8 +177,8 @@
 
       magma::Status Wait(uint64_t timeout_ms) override { return MAGMA_STATUS_OK; }
 
-      bool WaitAsync(magma::PlatformPort* platform_port) override {
-        return real_semaphore_->WaitAsync(platform_port);
+      bool WaitAsync(magma::PlatformPort* port, uint64_t* key_out) override {
+        return real_semaphore_->WaitAsync(port, key_out);
       }
       uint64_t id() override { return real_semaphore_->id(); }
       bool duplicate_handle(uint32_t* handle_out) override {
diff --git a/src/graphics/drivers/msd-intel-gen/tests/unit_tests/test_device.cc b/src/graphics/drivers/msd-intel-gen/tests/unit_tests/test_device.cc
index 160e561..8b0b36d 100644
--- a/src/graphics/drivers/msd-intel-gen/tests/unit_tests/test_device.cc
+++ b/src/graphics/drivers/msd-intel-gen/tests/unit_tests/test_device.cc
@@ -385,7 +385,7 @@
       return wait_return_.load();
     }
 
-    bool WaitAsync(magma::PlatformPort* platform_port) override { return false; }
+    bool WaitAsync(magma::PlatformPort* port, uint64_t* key_out) override { return false; }
 
     std::unique_ptr<magma::PlatformSemaphore> sem_ = magma::PlatformSemaphore::Create();
     std::unique_ptr<magma::PlatformSemaphore> signal_sem_ = magma::PlatformSemaphore::Create();
diff --git a/src/graphics/lib/magma/include/magma_abi/magma.h b/src/graphics/lib/magma/include/magma_abi/magma.h
index 8b0a2f3..7419eb0 100644
--- a/src/graphics/lib/magma/include/magma_abi/magma.h
+++ b/src/graphics/lib/magma/include/magma_abi/magma.h
@@ -345,7 +345,8 @@
     magma_semaphore_t semaphore);
 
 ///
-/// \brief Waits for one or all provided semaphores to be signaled. Does not reset any semaphores.
+/// \brief DEPRECATED. Waits for one or all provided semaphores to be signaled. Does not reset any
+///        semaphores.
 /// \param semaphores Array of valid semaphores.
 /// \param count Number of semaphores in the array.
 /// \param timeout_ms Time to wait before returning MAGMA_STATUS_TIMED_OUT.
@@ -390,8 +391,8 @@
     magma_connection_t connection);
 
 ///
-/// \brief Returns MAGMA_STATUS_OK if a message is available on the notification channel before the
-///        given timeout expires.
+/// \brief DEPRECATED. Returns MAGMA_STATUS_OK if a message is available on the notification channel
+///        before the given timeout expires.
 /// \param connection An open connection.
 /// \param timeout_ns Time to wait before returning MAGMA_STATUS_TIMED_OUT.
 ///
@@ -476,6 +477,22 @@
 magma_status_t magma_initialize_logging(
     magma_handle_t channel);
 
+///
+/// \brief Waits for at least one of the given items to meet a condition. Does not reset any
+///        semaphores. Results are returned in the items array.
+/// \param items Array of poll items. Type should be either MAGMA_POLL_TYPE_SEMAPHORE or
+///        MAGMA_POLL_TYPE_HANDLE. Condition may be set to MAGMA_POLL_CONDITION_SIGNALED OR
+///        MAGMA_POLL_CONDITION_READABLE. If condition is 0 the item is ignored. Item results are
+///        set to the condition that was satisfied, otherwise 0. If the same item is given twice the
+///        behavior is undefined.
+/// \param count Number of poll items in the array.
+/// \param timeout_ns Time in ns to wait before returning MAGMA_STATUS_TIMED_OUT.
+///
+magma_status_t magma_poll(
+    magma_poll_item_t* items,
+    uint32_t count,
+    uint64_t timeout_ns);
+
 #if defined(__cplusplus)
 }
 #endif
diff --git a/src/graphics/lib/magma/include/magma_abi/magma.json b/src/graphics/lib/magma/include/magma_abi/magma.json
index 6484e59..58950af 100644
--- a/src/graphics/lib/magma/include/magma_abi/magma.json
+++ b/src/graphics/lib/magma/include/magma_abi/magma.json
@@ -1,6 +1,6 @@
 {
   "magma-interface": {
-    "next-free-ordinal": 52,
+    "next-free-ordinal": 53,
     "exports": [
       {
         "ordinal": 4,
@@ -649,7 +649,7 @@
         "ordinal": 39,
         "name": "magma_wait_semaphores",
         "type": "magma_status_t",
-        "description": "Waits for one or all provided semaphores to be signaled. Does not reset any semaphores.",
+        "description": "DEPRECATED. Waits for one or all provided semaphores to be signaled. Does not reset any semaphores.",
         "arguments": [
           {
             "name": "semaphores",
@@ -736,7 +736,7 @@
         "ordinal": 43,
         "name": "magma_wait_notification_channel",
         "type": "magma_status_t",
-        "description": "Returns MAGMA_STATUS_OK if a message is available on the notification channel before the given timeout expires.",
+        "description": "DEPRECATED. Returns MAGMA_STATUS_OK if a message is available on the notification channel before the given timeout expires.",
         "arguments": [
           {
             "name": "connection",
@@ -898,6 +898,29 @@
             "description": "An open connection to the syslog service."
           }
         ]
+      },
+      {
+        "ordinal": 52,
+        "name": "magma_poll",
+        "type": "magma_status_t",
+        "description": "Waits for at least one of the given items to meet a condition. Does not reset any semaphores. Results are returned in the items array.",
+        "arguments": [
+          {
+            "name": "items",
+            "type": "magma_poll_item_t*",
+            "description": "Array of poll items. Type should be either MAGMA_POLL_TYPE_SEMAPHORE or MAGMA_POLL_TYPE_HANDLE. Condition may be set to MAGMA_POLL_CONDITION_SIGNALED OR MAGMA_POLL_CONDITION_READABLE. If condition is 0 the item is ignored. Item results are set to the condition that was satisfied, otherwise 0. If the same item is given twice the behavior is undefined."
+          },
+          {
+            "name": "count",
+            "type": "uint32_t",
+            "description": "Number of poll items in the array."
+          },
+          {
+            "name": "timeout_ns",
+            "type": "uint64_t",
+            "description": "Time in ns to wait before returning MAGMA_STATUS_TIMED_OUT."
+          }
+        ]
       }
     ]
   }
diff --git a/src/graphics/lib/magma/include/magma_abi/magma_common_defs.h b/src/graphics/lib/magma/include/magma_abi/magma_common_defs.h
index 8f0f305..ded5a7b 100644
--- a/src/graphics/lib/magma/include/magma_abi/magma_common_defs.h
+++ b/src/graphics/lib/magma/include/magma_abi/magma_common_defs.h
@@ -82,6 +82,13 @@
   MAGMA_COHERENCY_DOMAIN_RAM = 1,
 };
 
+enum { MAGMA_POLL_TYPE_SEMAPHORE = 1, MAGMA_POLL_TYPE_HANDLE = 2 };
+
+enum {
+  MAGMA_POLL_CONDITION_READABLE = 1,
+  MAGMA_POLL_CONDITION_SIGNALED = 3,
+};
+
 #define MAGMA_SYSMEM_FLAG_PROTECTED (1 << 0)
 #define MAGMA_SYSMEM_FLAG_DISPLAY (1 << 1)
 
@@ -117,6 +124,16 @@
 // Corresponds to a zx_handle_t on Fuchsia.
 typedef uint32_t magma_handle_t;
 
+typedef struct magma_poll_item {
+  union {
+    magma_semaphore_t semaphore;
+    magma_handle_t handle;
+  };
+  uint32_t type;
+  uint32_t condition;
+  uint32_t result;
+} magma_poll_item_t;
+
 // a buffer plus its associated relocations referenced by a command buffer
 struct magma_system_exec_resource {
   uint64_t buffer_id;
diff --git a/src/graphics/lib/magma/src/libmagma/magma.cc b/src/graphics/lib/magma/src/libmagma/magma.cc
index 871c496..e1174e7 100644
--- a/src/graphics/lib/magma/src/libmagma/magma.cc
+++ b/src/graphics/lib/magma/src/libmagma/magma.cc
@@ -5,12 +5,14 @@
 #include "magma.h"
 
 #include <chrono>
+#include <map>
 
 #include "magma_util/macros.h"
 #include "platform_connection_client.h"
 #include "platform_device_client.h"
 #include "platform_handle.h"
 #include "platform_logger.h"
+#include "platform_object.h"
 #include "platform_port.h"
 #include "platform_semaphore.h"
 #include "platform_thread.h"
@@ -385,6 +387,95 @@
   return MAGMA_STATUS_OK;
 }
 
+magma_status_t magma_poll(magma_poll_item_t* items, uint32_t count, uint64_t timeout_ns) {
+  // Optimize for simple case
+  if (count == 1 && items[0].type == MAGMA_POLL_TYPE_SEMAPHORE &&
+      items[0].condition == MAGMA_POLL_CONDITION_SIGNALED) {
+    items[0].result = 0;
+    // TODO(fxb/49103) change WaitNoReset to take ns
+    if (!reinterpret_cast<magma::PlatformSemaphore*>(items[0].semaphore)
+             ->WaitNoReset(magma::ns_to_ms(timeout_ns)))
+      return MAGMA_STATUS_TIMED_OUT;
+
+    items[0].result = items[0].condition;
+    return MAGMA_STATUS_OK;
+  }
+
+  std::unique_ptr<magma::PlatformPort> port = magma::PlatformPort::Create();
+  if (!port)
+    return DRET_MSG(MAGMA_STATUS_INTERNAL_ERROR, "Failed to create port");
+
+  // Map of key to item index
+  std::map<uint64_t, uint32_t> map;
+
+  for (uint32_t i = 0; i < count; i++) {
+    items[i].result = 0;
+
+    if (!items[i].condition)
+      continue;
+
+    switch (items[i].type) {
+      case MAGMA_POLL_TYPE_SEMAPHORE: {
+        if (items[i].condition != MAGMA_POLL_CONDITION_SIGNALED)
+          return DRET_MSG(MAGMA_STATUS_INVALID_ARGS, "Invalid condition for semaphore: 0x%x",
+                          items[i].condition);
+
+        auto semaphore = reinterpret_cast<magma::PlatformSemaphore*>(items[i].semaphore);
+        uint64_t key;
+        if (!semaphore->WaitAsync(port.get(), &key))
+          return DRET_MSG(MAGMA_STATUS_INTERNAL_ERROR, "WaitAsync failed");
+
+        map[key] = i;
+        break;
+      }
+
+      case MAGMA_POLL_TYPE_HANDLE: {
+        if (items[i].condition != MAGMA_POLL_CONDITION_READABLE)
+          return DRET_MSG(MAGMA_STATUS_INVALID_ARGS, "Invalid condition for handle: 0x%x",
+                          items[i].condition);
+
+        auto platform_handle = magma::PlatformHandle::Create(items[i].handle);
+        if (!platform_handle)
+          return DRET_MSG(MAGMA_STATUS_INTERNAL_ERROR, "Failed to create platform handle");
+
+        uint64_t key;
+        bool result = platform_handle->WaitAsync(port.get(), &key);
+        platform_handle->release();
+
+        if (!result)
+          return DRET_MSG(MAGMA_STATUS_INTERNAL_ERROR, "WaitAsync failed");
+
+        map[key] = i;
+        break;
+      }
+    }
+  }
+
+  if (map.empty())
+    return DRET_MSG(MAGMA_STATUS_INVALID_ARGS, "Nothing to do");
+
+  // TODO(fxb/49103) change PlatformPort::Wait to take ns
+  uint64_t key;
+  magma::Status status = port->Wait(&key, magma::ns_to_ms(timeout_ns));
+  if (!status)
+    return status.get();
+
+  while (status) {
+    auto iter = map.find(key);
+    if (iter == map.end())
+      return DRET_MSG(MAGMA_STATUS_INTERNAL_ERROR, "Couldn't find key in map: 0x%lx", key);
+
+    uint32_t index = iter->second;
+    DASSERT(index < count);
+    items[index].result = items[index].condition;
+
+    // Check for more events
+    status = port->Wait(&key, 0);
+  }
+
+  return MAGMA_STATUS_OK;
+}
+
 magma_status_t magma_export_semaphore(magma_connection_t connection, magma_semaphore_t semaphore,
                                       uint32_t* semaphore_handle_out) {
   auto platform_semaphore = reinterpret_cast<magma::PlatformSemaphore*>(semaphore);
diff --git a/src/graphics/lib/magma/src/libmagma_linux/magma.cc b/src/graphics/lib/magma/src/libmagma_linux/magma.cc
index f1e0ff2..942879a 100644
--- a/src/graphics/lib/magma/src/libmagma_linux/magma.cc
+++ b/src/graphics/lib/magma/src/libmagma_linux/magma.cc
@@ -103,6 +103,10 @@
   return result_return;
 }
 
+magma_status_t magma_poll(magma_poll_item_t* items, uint32_t count, uint64_t timeout_ns) {
+  return MAGMA_STATUS_UNIMPLEMENTED;
+}
+
 void magma_execute_command_buffer_with_resources(magma_connection_t connection, uint32_t context_id,
                                                  struct magma_system_command_buffer* command_buffer,
                                                  struct magma_system_exec_resource* resources,
diff --git a/src/graphics/lib/magma/src/magma_util/platform/BUILD.gn b/src/graphics/lib/magma/src/magma_util/platform/BUILD.gn
index e3e62e1..437d207 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/BUILD.gn
+++ b/src/graphics/lib/magma/src/magma_util/platform/BUILD.gn
@@ -108,10 +108,15 @@
   public_configs = [ ":platform_include_config" ]
 
   sources = [ "platform_handle.h" ]
+
+  public_deps = [ ":port_header" ]
 }
 
 source_set("handle") {
-  public_deps = [ ":handle_header" ]
+  public_deps = [
+    ":handle_header",
+    ":port",
+  ]
 
   if (is_fuchsia) {
     deps = [ "zircon:handle" ]
diff --git a/src/graphics/lib/magma/src/magma_util/platform/platform_handle.h b/src/graphics/lib/magma/src/magma_util/platform/platform_handle.h
index 7e481fd..117cffd 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/platform_handle.h
+++ b/src/graphics/lib/magma/src/magma_util/platform/platform_handle.h
@@ -7,6 +7,8 @@
 
 #include <memory>
 
+#include "platform_port.h"
+
 namespace magma {
 
 class PlatformHandle {
@@ -17,6 +19,11 @@
   virtual bool GetCount(uint32_t* count_out) = 0;
   virtual uint32_t release() = 0;
 
+  // Registers an async wait delivered on the given |port| when the given handle is readable,
+  // or if the handle has a peer and the peer is closed.
+  // On success returns true and |key_out| is set.
+  virtual bool WaitAsync(PlatformPort* port, uint64_t* key_out) = 0;
+
   static bool duplicate_handle(uint32_t handle_in, uint32_t* handle_out);
 
   static std::unique_ptr<PlatformHandle> Create(uint32_t handle);
diff --git a/src/graphics/lib/magma/src/magma_util/platform/platform_semaphore.h b/src/graphics/lib/magma/src/magma_util/platform/platform_semaphore.h
index d999df2..5bd2aad 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/platform_semaphore.h
+++ b/src/graphics/lib/magma/src/magma_util/platform/platform_semaphore.h
@@ -68,7 +68,13 @@
 
   // Registers an async wait delivered on the given port when this semaphore is signalled.
   // Note that a port wait completion will not autoreset the semaphore.
-  virtual bool WaitAsync(PlatformPort* platform_port) = 0;
+  // On success returns true and |key_out| is set.
+  virtual bool WaitAsync(PlatformPort* port, uint64_t* key_out) = 0;
+
+  bool WaitAsync(PlatformPort* port) {
+    uint64_t key;
+    return WaitAsync(port, &key);
+  }
 };
 
 }  // namespace magma
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/BUILD.gn b/src/graphics/lib/magma/src/magma_util/platform/zircon/BUILD.gn
index 1a4dba9..8fbca83 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/BUILD.gn
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/BUILD.gn
@@ -225,12 +225,16 @@
   ]
 
   public_deps = [
+    ":port",
     "$magma_build_root/src/magma_util:macros",
     "$zircon_build_root/public/lib/zx",
     "..:handle_header",
   ]
 
-  deps = [ "$magma_build_root/src/magma_util:macros" ]
+  deps = [
+    "$magma_build_root/src/magma_util:macros",
+    "..:object",
+  ]
 }
 
 source_set("iommu") {
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.cc b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.cc
index 6437abb..72d3dc5 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.cc
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.cc
@@ -6,6 +6,9 @@
 
 #include <lib/zx/handle.h>
 
+#include "platform_object.h"
+#include "zircon_platform_port.h"
+
 namespace magma {
 
 bool ZirconPlatformHandle::GetCount(uint32_t* count_out) {
@@ -19,6 +22,20 @@
   return true;
 }
 
+bool ZirconPlatformHandle::WaitAsync(PlatformPort* port, uint64_t* key_out) {
+  if (!PlatformObject::IdFromHandle(get(), key_out))
+    return DRET_MSG(false, "IdFromHandle failed");
+
+  auto zircon_port = static_cast<ZirconPlatformPort*>(port);
+  zx_status_t status =
+      handle_.wait_async(zircon_port->zx_port(), *key_out,
+                         ZX_CHANNEL_READABLE | ZX_CHANNEL_PEER_CLOSED, ZX_WAIT_ASYNC_ONCE);
+  if (status != ZX_OK)
+    return DRETF(false, "wait_async failed: %d", status);
+
+  return true;
+}
+
 // static
 bool PlatformHandle::duplicate_handle(uint32_t handle_in, uint32_t* handle_out) {
   zx_status_t status = zx_handle_duplicate(handle_in, ZX_RIGHT_SAME_RIGHTS, handle_out);
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.h b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.h
index 6a8562a..eb6484f 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.h
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_handle.h
@@ -20,6 +20,8 @@
 
   bool GetCount(uint32_t* count_out) override;
 
+  bool WaitAsync(PlatformPort* port, uint64_t* key_out) override;
+
   uint32_t release() override { return handle_.release(); }
 
   zx_handle_t get() { return handle_.get(); }
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_port.cc b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_port.cc
index 42325f7..99af079 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_port.cc
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_port.cc
@@ -19,8 +19,11 @@
   zx_port_packet_t packet;
   zx_status_t status =
       port_.wait(zx::deadline_after(zx::duration(magma::ms_to_signed_ns(timeout_ms))), &packet);
-  if (status == ZX_ERR_TIMED_OUT)
-    return DRET_MSG(MAGMA_STATUS_TIMED_OUT, "port wait timed out");
+  if (status == ZX_ERR_TIMED_OUT) {
+    return timeout_ms == 0 ? MAGMA_STATUS_TIMED_OUT
+                           : DRET_MSG(MAGMA_STATUS_TIMED_OUT, "port wait timed out: timeout_ms %lu",
+                                      timeout_ms);
+  }
 
   DLOG("port received key 0x%" PRIx64 " status %d", packet.key, status);
 
@@ -31,6 +34,9 @@
     // Port has been closed.
     port_.reset();
     return MAGMA_STATUS_INTERNAL_ERROR;
+  } else if (packet.type == ZX_PKT_TYPE_SIGNAL_ONE &&
+             packet.signal.observed == ZX_CHANNEL_PEER_CLOSED) {
+    return DRET(MAGMA_STATUS_CONNECTION_LOST);
   }
 
   *key_out = packet.key;
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.cc b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.cc
index 1b6c320..5d27839 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.cc
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.cc
@@ -45,15 +45,19 @@
   return status;
 }
 
-bool ZirconPlatformSemaphore::WaitAsync(PlatformPort* platform_port) {
+bool ZirconPlatformSemaphore::WaitAsync(PlatformPort* port, uint64_t* key_out) {
   TRACE_DURATION("magma:sync", "semaphore wait async", "id", koid_);
   TRACE_FLOW_BEGIN("magma:sync", "semaphore wait async", koid_);
 
-  auto port = static_cast<ZirconPlatformPort*>(platform_port);
-  zx_status_t status = event_.wait_async(port->zx_port(), id(), zx_signal(), ZX_WAIT_ASYNC_ONCE);
+  auto zircon_port = static_cast<ZirconPlatformPort*>(port);
+
+  uint64_t key = id();
+  zx_status_t status =
+      event_.wait_async(zircon_port->zx_port(), key, zx_signal(), ZX_WAIT_ASYNC_ONCE);
   if (status != ZX_OK)
     return DRETF(false, "wait_async failed: %d", status);
 
+  *key_out = key;
   return true;
 }
 
diff --git a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.h b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.h
index 8a52f60..f3cdf8f 100644
--- a/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.h
+++ b/src/graphics/lib/magma/src/magma_util/platform/zircon/zircon_platform_semaphore.h
@@ -39,7 +39,7 @@
   magma::Status WaitNoReset(uint64_t timeout_ms) override;
   magma::Status Wait(uint64_t timeout_ms) override;
 
-  bool WaitAsync(PlatformPort* platform_port) override;
+  bool WaitAsync(PlatformPort* port, uint64_t* key_out) override;
 
   zx_handle_t zx_handle() const { return event_.get(); }
 
diff --git a/src/graphics/lib/magma/tests/integration/test_magma_abi.cc b/src/graphics/lib/magma/tests/integration/test_magma_abi.cc
index 56a41a1..2e126b6 100644
--- a/src/graphics/lib/magma/tests/integration/test_magma_abi.cc
+++ b/src/graphics/lib/magma/tests/integration/test_magma_abi.cc
@@ -32,6 +32,7 @@
 
 inline uint64_t page_size() { return sysconf(_SC_PAGESIZE); }
 
+inline constexpr int64_t ms_to_ns(int64_t ms) { return ms * 1000000ull; }
 }  // namespace
 
 class TestConnection {
@@ -286,6 +287,149 @@
     }
   }
 
+  void PollWithNotificationChannel(uint32_t semaphore_count) {
+    ASSERT_TRUE(connection_);
+
+    std::vector<magma_poll_item_t> items;
+
+    for (uint32_t i = 0; i < semaphore_count; i++) {
+      magma_semaphore_t semaphore;
+      ASSERT_EQ(MAGMA_STATUS_OK, magma_create_semaphore(connection_, &semaphore));
+
+      items.push_back({.semaphore = semaphore,
+                       .type = MAGMA_POLL_TYPE_SEMAPHORE,
+                       .condition = MAGMA_POLL_CONDITION_SIGNALED});
+    }
+
+    items.push_back({
+        .handle = magma_get_notification_channel_handle(connection_),
+        .type = MAGMA_POLL_TYPE_HANDLE,
+        .condition = MAGMA_POLL_CONDITION_READABLE,
+    });
+
+    constexpr int64_t kTimeoutNs = ms_to_ns(100);
+    auto start = std::chrono::steady_clock::now();
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, magma_poll(items.data(), items.size(), kTimeoutNs));
+    EXPECT_LE(kTimeoutNs, std::chrono::duration_cast<std::chrono::nanoseconds>(
+                              std::chrono::steady_clock::now() - start)
+                              .count());
+
+    magma_signal_semaphore(items[0].semaphore);
+
+    EXPECT_EQ(MAGMA_STATUS_OK, magma_poll(items.data(), items.size(), 0));
+    EXPECT_EQ(items[0].result, items[0].condition);
+    EXPECT_EQ(items[1].result, 0u);
+
+    magma_reset_semaphore(items[0].semaphore);
+
+    start = std::chrono::steady_clock::now();
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, magma_poll(items.data(), items.size(), kTimeoutNs));
+    EXPECT_LE(kTimeoutNs, std::chrono::duration_cast<std::chrono::nanoseconds>(
+                              std::chrono::steady_clock::now() - start)
+                              .count());
+
+    for (uint32_t i = 0; i < semaphore_count; i++) {
+      magma_signal_semaphore(items[i].semaphore);
+    }
+
+    EXPECT_EQ(MAGMA_STATUS_OK, magma_poll(items.data(), items.size(), 0));
+
+    for (uint32_t i = 0; i < items.size(); i++) {
+      if (i < items.size() - 1) {
+        EXPECT_EQ(items[i].result, items[i].condition);
+      } else {
+        // Notification channel
+        EXPECT_EQ(items[i].result, 0u);
+      }
+    }
+
+    for (uint32_t i = 0; i < semaphore_count; i++) {
+      magma_release_semaphore(connection_, items[i].semaphore);
+    }
+  }
+
+  void PollWithTestChannel() {
+#ifdef __Fuchsia__
+    ASSERT_TRUE(connection_);
+
+    zx::channel local, remote;
+    ASSERT_EQ(ZX_OK, zx::channel::create(0 /* flags */, &local, &remote));
+
+    magma_semaphore_t semaphore;
+    ASSERT_EQ(MAGMA_STATUS_OK, magma_create_semaphore(connection_, &semaphore));
+
+    std::vector<magma_poll_item_t> items;
+    items.push_back({.semaphore = semaphore,
+                     .type = MAGMA_POLL_TYPE_SEMAPHORE,
+                     .condition = MAGMA_POLL_CONDITION_SIGNALED});
+    items.push_back({
+        .handle = local.get(),
+        .type = MAGMA_POLL_TYPE_HANDLE,
+        .condition = MAGMA_POLL_CONDITION_READABLE,
+    });
+
+    constexpr int64_t kTimeoutNs = ms_to_ns(100);
+    auto start = std::chrono::steady_clock::now();
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, magma_poll(items.data(), items.size(), kTimeoutNs));
+    EXPECT_LE(kTimeoutNs, std::chrono::duration_cast<std::chrono::nanoseconds>(
+                              std::chrono::steady_clock::now() - start)
+                              .count());
+
+    magma_signal_semaphore(semaphore);
+
+    EXPECT_EQ(MAGMA_STATUS_OK, magma_poll(items.data(), items.size(), 0));
+    EXPECT_EQ(items[0].result, items[0].condition);
+    EXPECT_EQ(items[1].result, 0u);
+
+    magma_reset_semaphore(semaphore);
+
+    start = std::chrono::steady_clock::now();
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, magma_poll(items.data(), items.size(), kTimeoutNs));
+    EXPECT_LE(kTimeoutNs, std::chrono::duration_cast<std::chrono::nanoseconds>(
+                              std::chrono::steady_clock::now() - start)
+                              .count());
+
+    uint32_t dummy;
+    EXPECT_EQ(ZX_OK, remote.write(0 /* flags */, &dummy, sizeof(dummy), nullptr /* handles */,
+                                  0 /* num_handles*/));
+
+    EXPECT_EQ(MAGMA_STATUS_OK, magma_poll(items.data(), items.size(), 0));
+    EXPECT_EQ(items[0].result, 0u);
+    EXPECT_EQ(items[1].result, items[1].condition);
+
+    magma_signal_semaphore(semaphore);
+
+    EXPECT_EQ(MAGMA_STATUS_OK, magma_poll(items.data(), items.size(), 0));
+    EXPECT_EQ(items[0].result, items[0].condition);
+    EXPECT_EQ(items[1].result, items[1].condition);
+#else
+    GTEST_SKIP();
+#endif
+  }
+
+  void PollChannelClosed() {
+#ifdef __Fuchsia__
+    ASSERT_TRUE(connection_);
+
+    zx::channel local, remote;
+    ASSERT_EQ(ZX_OK, zx::channel::create(0 /* flags */, &local, &remote));
+
+    std::vector<magma_poll_item_t> items;
+    items.push_back({
+        .handle = local.get(),
+        .type = MAGMA_POLL_TYPE_HANDLE,
+        .condition = MAGMA_POLL_CONDITION_READABLE,
+    });
+
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, magma_poll(items.data(), items.size(), 0));
+
+    remote.reset();
+    EXPECT_EQ(MAGMA_STATUS_CONNECTION_LOST, magma_poll(items.data(), items.size(), 0));
+#else
+    GTEST_SKIP();
+#endif
+  }
+
   void SemaphoreExport(uint32_t* handle_out, uint64_t* id_out) {
     ASSERT_NE(connection_, nullptr);
     magma_semaphore_t semaphore;
@@ -646,6 +790,16 @@
 
 TEST(MagmaAbi, ImmediateCommands) { TestConnection().ImmediateCommands(); }
 
+TEST(MagmaAbi, PollWithNotificationChannel) {
+  TestConnection().PollWithNotificationChannel(1);
+  TestConnection().PollWithNotificationChannel(2);
+  TestConnection().PollWithNotificationChannel(3);
+}
+
+TEST(MagmaAbi, PollWithTestChannel) { TestConnection().PollWithTestChannel(); }
+
+TEST(MagmaAbi, PollChannelClosed) { TestConnection().PollChannelClosed(); }
+
 TEST(MagmaAbi, ImageFormat) {
   TestConnection test;
   test.ImageFormat();
diff --git a/src/graphics/lib/magma/tests/mock/mock_magma_system.cc b/src/graphics/lib/magma/tests/mock/mock_magma_system.cc
index 50b7093..f83b635 100644
--- a/src/graphics/lib/magma/tests/mock/mock_magma_system.cc
+++ b/src/graphics/lib/magma/tests/mock/mock_magma_system.cc
@@ -365,3 +365,7 @@
 magma_status_t magma_initialize_logging(magma_handle_t channel) {
   return MAGMA_STATUS_UNIMPLEMENTED;
 }
+
+magma_status_t magma_poll(magma_poll_item_t* items, uint32_t count, uint64_t timeout_ns) {
+  return MAGMA_STATUS_UNIMPLEMENTED;
+}
diff --git a/src/graphics/lib/magma/tests/unit_tests/test_platform_port.cc b/src/graphics/lib/magma/tests/unit_tests/test_platform_port.cc
index 65081d6..996baf5 100644
--- a/src/graphics/lib/magma/tests/unit_tests/test_platform_port.cc
+++ b/src/graphics/lib/magma/tests/unit_tests/test_platform_port.cc
@@ -10,6 +10,10 @@
 #include "platform_port.h"
 #include "platform_semaphore.h"
 
+#ifdef __Fuchsia__
+#include <lib/zx/channel.h>
+#endif
+
 namespace {
 
 class TestPort {
@@ -92,8 +96,53 @@
     }));
     thread->join();
   }
+
+  static void TestHandle() {
+#ifdef __Fuchsia__
+    zx::channel local, remote;
+    ASSERT_EQ(ZX_OK, zx::channel::create(0 /*flags*/, &local, &remote));
+
+    auto handle = magma::PlatformHandle::Create(local.release());
+    ASSERT_TRUE(handle);
+
+    auto port = magma::PlatformPort::Create();
+    ASSERT_TRUE(port);
+
+    uint64_t handle_key;
+    EXPECT_TRUE(handle->WaitAsync(port.get(), &handle_key));
+
+    uint64_t key;
+    EXPECT_EQ(MAGMA_STATUS_TIMED_OUT, port->Wait(&key, 0).get());
+
+    uint32_t dummy;
+    EXPECT_EQ(ZX_OK, remote.write(0 /* flags */, &dummy, sizeof(dummy), nullptr /* handles */,
+                                  0 /* num_handles*/));
+
+    // Close the peer
+    remote.reset();
+
+    EXPECT_EQ(MAGMA_STATUS_OK, port->Wait(&key, 0).get());
+    EXPECT_EQ(handle_key, key);
+
+    local.reset(handle->release());
+
+    uint32_t actual_bytes;
+    EXPECT_EQ(ZX_OK, local.read(0 /* flags */, &dummy, nullptr /*handles*/, sizeof(dummy),
+                                0 /*num_handles*/, &actual_bytes, nullptr /*actual_handles*/));
+
+    handle = magma::PlatformHandle::Create(local.release());
+
+    EXPECT_TRUE(handle->WaitAsync(port.get(), &handle_key));
+
+    EXPECT_EQ(MAGMA_STATUS_CONNECTION_LOST, port->Wait(&key, 0).get());
+#else
+    GTEST_SKIP();
+#endif
+  }
 };
 
 }  // namespace
 
 TEST(PlatformPort, Test) { TestPort::Test(); }
+
+TEST(PlatformPort, Handle) { TestPort::TestHandle(); }