unity/Assets/Scripts/RenderServer.cs (374 lines of code) (raw):

using System.Collections.Generic; using System.Threading.Tasks; using Grpc.Core; using UnityEngine; using StreamEntry = Orrb.RenderBatchResponse.Types.StreamEntry; using BatchResponseEntry = Orrb.RenderBatchResponse.Types.StreamEntry.Types.BatchResponseEntry; using Google.Protobuf; using System.Threading; using System.IO; using System; // The RenderServer starts a GRPC service and processes incoming // RenderBatch and UpdateRenderer requests. The GRPC servers are // inherently asynchronous and the Unity game loop is embarassingly // serial. To join those two worlds the GRPC server communicates with // the game loop over a concurrent queue. The incoming requests wait // on enqueued workloads. The game loop processes the workloads in // a serial fashion and then fulfills a Response promise. The queue // will spin for a little while and then use user space conditional // variables, in order to reduce context switching and provide highest // performance in high load scenarios. When idle the queue will wait // on the conditional variable with high timeout, reducing the idle // load significantly. // // Configurable flags: // int queues_count - GRPC completion queue count, // int workers_count - GRPC worker threads initial count, // int streams_count - GRPC concurrent streams max count, // int port - port to bind the render service to, // string host - host to bind the render service to. public class RenderServer : MonoBehaviour, IImageBatchConsumer { private interface IRenderServerWorkload { void InitializeWorkload(); // One frame of processing. void ProcessWorkload(); // Has this workload finished? bool WorkloadDone(); } private class QueuedWorkloadRequest<Request, Response> where Request : class { public RenderServer server_ = null; // The GRPC server hangs on this promise, signal it when the work // is done and the response can be sent back. public TaskCompletionSource<Response> response_promise_ = new TaskCompletionSource<Response>(); public Request request_ = null; public QueuedWorkloadRequest(RenderServer server, Request request) { server_ = server; request_ = request; } } // This structure gathers auxiliary outputs. It assumes that the per // frame output from a given stream will have constant size. It also // assumes that all frames generate the same output streams. private class BatchOutputContext : RendererComponent.IOutputContext { private int batch_size_ = 0; private int current_entry_ = 0; private class OutputStreams<T> where T : struct { private Dictionary<string, T[]> streams_ = new Dictionary<string, T[]>(); public Dictionary<string, T[]> GetStreams() { return streams_; } private bool EnsureStream(string stream_name, int stream_size, int entry, int entry_length) { if (streams_.ContainsKey(stream_name)) { T[] stream = streams_[stream_name]; if (stream.Length != stream_size * entry_length) { Logger.Error("RenderServer::BatchOutputContext::EnsureStream::Wrong size, {0} vs. {1}x{2}.", stream.Length, stream_size, entry_length); return false; } return true; } else if (entry == 0) { streams_.Add(stream_name, new T[stream_size * entry_length]); return true; } else { Logger.Error("RenderServer::BatchOutputContext::EnsureStream::Entry not 0, in initialize."); return false; } } public bool Output(string stream_name, int stream_size, int entry, T value) { if (!EnsureStream(stream_name, stream_size, entry, 1)) { Logger.Error("RenderServer::BatchOutputContext::Output::Cannot output to stream: {0}", stream_name); return false; } streams_[stream_name][entry] = value; return true; } public bool Output(string stream_name, int stream_size, int entry, T[] values) { if (!EnsureStream(stream_name, stream_size, entry, values.Length)) { Logger.Error("RenderServer::BatchOutputContext::Output::Cannot output to stream: {0}", stream_name); return false; } Array.Copy(values, 0, streams_[stream_name], values.Length * entry, values.Length); return true; } }; private OutputStreams<int> int_streams_ = null; private OutputStreams<float> float_streams_ = null; private OutputStreams<bool> bool_streams_ = null; public BatchOutputContext(int batch_size) { this.batch_size_ = batch_size; this.int_streams_ = new OutputStreams<int>(); this.float_streams_ = new OutputStreams<float>(); this.bool_streams_ = new OutputStreams<bool>(); } public void Advance() { this.current_entry_++; } public void OutputBool(string output_name, bool value) { bool_streams_.Output(output_name, batch_size_, current_entry_, value); } public void OutputBools(string output_name, bool[] values) { bool_streams_.Output(output_name, batch_size_, current_entry_, values); } public void OutputFloat(string output_name, float value) { float_streams_.Output(output_name, batch_size_, current_entry_, value); } public void OutputFloats(string output_name, float[] values) { float_streams_.Output(output_name, batch_size_, current_entry_, values); } public void OutputInt(string output_name, int value) { int_streams_.Output(output_name, batch_size_, current_entry_, value); } public void OutputInts(string output_name, int[] values) { int_streams_.Output(output_name, batch_size_, current_entry_, values); } public void BuildResponseStreams(Orrb.RenderBatchResponse response) { foreach (KeyValuePair<string, bool[]> bool_stream in bool_streams_.GetStreams()) { Orrb.RenderBatchResponse.Types.AuxiliaryBoolStreamEntry response_bool_stream = new Orrb.RenderBatchResponse.Types.AuxiliaryBoolStreamEntry(); response_bool_stream.Name = bool_stream.Key; response_bool_stream.Data.AddRange(bool_stream.Value); response.AuxiliaryBoolStreams.Add(response_bool_stream); } foreach (KeyValuePair<string, int[]> int_stream in int_streams_.GetStreams()) { Orrb.RenderBatchResponse.Types.AuxiliaryIntStreamEntry response_int_stream = new Orrb.RenderBatchResponse.Types.AuxiliaryIntStreamEntry(); response_int_stream.Name = int_stream.Key; response_int_stream.Data.AddRange(int_stream.Value); response.AuxiliaryIntStreams.Add(response_int_stream); } foreach (KeyValuePair<string, float[]> float_stream in float_streams_.GetStreams()) { Orrb.RenderBatchResponse.Types.AuxiliaryFloatStreamEntry response_float_stream = new Orrb.RenderBatchResponse.Types.AuxiliaryFloatStreamEntry(); response_float_stream.Name = float_stream.Key; response_float_stream.Data.AddRange(float_stream.Value); response.AuxiliaryFloatStreams.Add(response_float_stream); } } } // This workload runs the work triggered by a RenderBatch RPC. private class RenderBatchWorkload : QueuedWorkloadRequest<Orrb.RenderBatchRequest, Orrb.RenderBatchResponse>, IRenderServerWorkload, IImageBatchConsumer { private float start_time_ = 0.0f; private int current_batch_entry_ = 0; private BatchOutputContext output_context_ = null; private bool done_ = false; public RenderBatchWorkload(RenderServer server, Orrb.RenderBatchRequest request) : base(server, request) { } public void InitializeWorkload() { Logger.Info("RenderBatchWorkload::InitializeWorkload::New render request."); start_time_ = Time.realtimeSinceStartup; output_context_ = new BatchOutputContext(request_.Entries.Count); current_batch_entry_ = 0; List<Camera> cameras = server_.scene_instance_.GetCameras(request_.CameraNames); // Make sure we can find all the requested cameras in the scene. if (cameras.Count != request_.CameraNames.Count) { Logger.Error("RenderServer::RenderBatchWorkload::InitializeWorkload::Cannot find all requested cameras."); done_ = true; return; } // Prepare the recorder, so that it has buffers ready. server_.recorder_.ResetBatch(cameras, request_.Entries.Count, request_.Width, request_.Height, request_.RenderAlpha, request_.RenderDepth, request_.RenderNormals, request_.RenderSegmentation); } // Render one state (frame). public void ProcessWorkload() { int seed = request_.BatchSeed + current_batch_entry_; if (request_.UseEntrySeeds) { seed = request_.Entries[current_batch_entry_].Seed; } UnityEngine.Random.InitState(seed); server_.scene_instance_.UpdateState(request_.Entries[current_batch_entry_].Qpos); server_.scene_instance_.GetComponentManager().RunComponents(output_context_); output_context_.Advance(); server_.recorder_.Capture(); current_batch_entry_++; } public bool WorkloadDone() { return done_; } // The RenderBatchWorkload is also an ImageBatchConsumer, when the // Recorder is done it will send the batch here (through the RenderServer). public void ConsumeImageBatch(RenderBatch batch) { Orrb.RenderBatchResponse response = new Orrb.RenderBatchResponse(); // Build the GRPC response from the recorded images... int frames = 0; foreach (KeyValuePair<string, RenderBatch.CameraBatch> pair in batch.camera_batches_) { Tuple<int, StreamEntry> stream_info = StreamFromBatch(pair.Key, pair.Value); response.Streams.Add(stream_info.Item2); frames += stream_info.Item1; } // ... and the auxiliary outputs. output_context_.BuildResponseStreams(response); float delta_time = Time.realtimeSinceStartup - start_time_; response_promise_.SetResult(response); Logger.Info("RenderBatchWorkload::ConsumeImageBatch::Batch finished: {0} images in {1} ({2}).", frames, delta_time, frames / delta_time); done_ = true; } private static Tuple<int, StreamEntry> StreamFromBatch(string name, RenderBatch.CameraBatch batch_stream) { StreamEntry stream = new StreamEntry(); stream.Name = name; int count = 0; foreach (KeyValuePair<RenderBatch.CameraBatch.RenderType, List<Texture2D>> pair in batch_stream.images_) { int i = 0; foreach (Texture2D image in pair.Value) { BatchResponseEntry entry; if (i + 1 > stream.Entries.Count) { entry = new BatchResponseEntry(); stream.Entries.Add(entry); } else { entry = stream.Entries[i]; } ++i; ByteString data = null; switch (pair.Key) { case RenderBatch.CameraBatch.RenderType.RGB: data = ByteString.CopyFrom(image.GetRawTextureData()); entry.ImageData = data; break; case RenderBatch.CameraBatch.RenderType.DEPTH: entry.DepthData = ReadDepth(image); break; case RenderBatch.CameraBatch.RenderType.NORMALS: entry.NormalsData = ReadNormals(image); break; case RenderBatch.CameraBatch.RenderType.SEGMENTATION: entry.SegmentationData = ReadSegmentation(image); break; default: Logger.Error("Unsupported Batch.Stream.Type {0}", pair.Key); break; } } count = i; } Tuple<int, StreamEntry> stream_info = new Tuple<int, StreamEntry>(count, stream); return stream_info; } private static ByteString ReadDepth(Texture2D texture) { // Read depth from RGBAFloat texture where its stored in R channel int size = texture.width * texture.height; byte[] depth_array = new byte[size * 4]; // float32, so 4 bytes byte[] texture_array = texture.GetRawTextureData(); for (int i = 0; i < size; ++i) { for (int j = 0; j < 4; ++j) { depth_array[i * 4 + j] = texture_array[i * 16 + j]; } } return ByteString.CopyFrom(depth_array); } private static ByteString ReadNormals(Texture2D texture) { // Read surface normals from RGBAFloat texture where they're stored in GBA channels int size = texture.width * texture.height; byte[] normals_array = new byte[size * 3 * 4]; // 3 times float32, so 3 * 4 bytes byte[] texture_array = texture.GetRawTextureData(); for (int i = 0; i < size; ++i) { for (int j = 0; j < 12; ++j) { // offset by 4 since first channel is depth normals_array[i * 12 + j] = texture_array[i * 16 + 4 + j]; } } return ByteString.CopyFrom(normals_array); } private static ByteString ReadSegmentation(Texture2D texture) { int size = texture.width * texture.height; byte[] segmentation_array = new byte[size]; byte[] texture_array = texture.GetRawTextureData(); for (int i = 0; i < size; ++i) { segmentation_array[i] = texture_array[i * 3]; // 3 RGB channels } return ByteString.CopyFrom(segmentation_array); } } // This instant workload updates the RenderComponentConfigs of // Components in the ComponentManager. private class UpdateWorkload : QueuedWorkloadRequest<Orrb.UpdateRequest, Orrb.UpdateResponse>, IRenderServerWorkload { public UpdateWorkload(RenderServer server, Orrb.UpdateRequest request) : base(server, request) { } public void InitializeWorkload() { } public void ProcessWorkload() { ComponentManager manager = server_.scene_instance_.GetComponentManager(); foreach (Orrb.RendererComponent config in request_.Components) { manager.UpdateComponent(config.Name, config.Config); } JsonFormatter formatter = new JsonFormatter(JsonFormatter.Settings.Default); Logger.Info("UpdateWorkload::ProcessWorkload::New config after update: {0}", formatter.Format(manager.GetConfig())); response_promise_.SetResult(new Orrb.UpdateResponse()); } public bool WorkloadDone() { return true; } } // GRPC RenderService implementation, just a proxy that that delegates // to the parent RenderServer class. private class RenderServiceImpl : Orrb.RenderService.RenderServiceBase { private RenderServer server_ = null; public RenderServiceImpl(RenderServer server) { server_ = server; } public override Task<Orrb.RenderBatchResponse> RenderBatch(Orrb.RenderBatchRequest request, ServerCallContext context) { RenderBatchWorkload workload = new RenderBatchWorkload(server_, request); server_.EnqueueWorkload(workload); return workload.response_promise_.Task; } public override Task<Orrb.UpdateResponse> Update(Orrb.UpdateRequest request, ServerCallContext context) { UpdateWorkload workload = new UpdateWorkload(server_, request); server_.EnqueueWorkload(workload); return workload.response_promise_.Task; } } [SerializeField] [Flag] public int queues_count_ = 4; [SerializeField] [Flag] public int workers_count_ = 4; [SerializeField] [Flag] public int streams_count_ = 4; [SerializeField] [Flag] public int port_ = 6666; [SerializeField] [Flag] public string host_ = "[::]"; private Recorder recorder_ = null; private SceneInstance scene_instance_ = null; private RenderServiceImpl render_service_ = null; private Server server_ = null; private IRenderServerWorkload current_workload_ = null; private Queue<IRenderServerWorkload> queue_ = new Queue<IRenderServerWorkload>(); // Use this for initialization void Start() { Flags.InitFlags(this, "render_server"); } public bool Initialize(Recorder recorder, SceneInstance scene_instance) { recorder_ = recorder; scene_instance_ = scene_instance; GrpcEnvironment.SetThreadPoolSize(workers_count_); GrpcEnvironment.SetCompletionQueueCount(queues_count_); render_service_ = new RenderServiceImpl(this); // Port reuse is turned off, it was a frequent cause of hard // to debug complications. server_ = new Server( new[] { new ChannelOption(ChannelOptions.SoReuseport, 0), new ChannelOption(ChannelOptions.MaxConcurrentStreams, streams_count_) }) { Services = { Orrb.RenderService.BindService(render_service_) }, Ports = { new ServerPort(host_, port_, ServerCredentials.Insecure) } }; try { server_.Start(); } catch (IOException e) { Logger.Error("RenderServer::Initialize::Failed to initialize render server on: {0}:{1} ({2})", host_, port_, e.Message); return false; } Logger.Info("RenderServer::Initialize::Initialized render server on: {0}:{1}", host_, port_); return true; } // Main server loop. Process current workload, or if it is done // try to get a next one. public void ProcessRequests() { if (current_workload_ != null) { ProcessCurrentWorkload(); } else { IRenderServerWorkload next_workload = GetNextWorkload(); if (next_workload != null) { InitializeNewWorkload(next_workload); ProcessCurrentWorkload(); } } } // The GRPC service will use this to enqueue and notify the main // loop of new incoming work. private void EnqueueWorkload(IRenderServerWorkload workload) { lock (queue_) { queue_.Enqueue(workload); Monitor.Pulse(queue_); } } // The main loop will use this to pull a new workload from the queue, // or wait (in a blocking fashion) till next one comes. private IRenderServerWorkload GetNextWorkload() { lock (queue_) { int retries = 10; while (retries-- > 0) { if (queue_.Count > 0) { return queue_.Dequeue(); } // Yield on the queue, up to 100ms, this reduces idle // load when no work is pending. Monitor.Wait(queue_, 100); } } return null; } private void InitializeNewWorkload(IRenderServerWorkload new_workload) { current_workload_ = new_workload; current_workload_.InitializeWorkload(); } private void ProcessCurrentWorkload() { current_workload_.ProcessWorkload(); if (current_workload_.WorkloadDone()) { current_workload_ = null; } } // The RenderServer is a ImageBatchConsumer, when the Recorder is done // it will send a batch here. Pass it to the current workload if it is // a ImageBatchConsumer too. public void ConsumeImageBatch(RenderBatch batch) { if (current_workload_ != null && current_workload_ is IImageBatchConsumer) { (current_workload_ as IImageBatchConsumer).ConsumeImageBatch(batch); if (current_workload_.WorkloadDone()) { current_workload_ = null; } } else { Logger.Warning("RenderServer::ConsumeImageBatch::Unexpected image batch consume call."); } } }