#include "Core/architecture/Frame_Scheduler.h" #include "Core/architecture/Triple_Buffer.h" #include "Core/plot/Latency_Eager_Refresh_Strategy.h" #include "Core/plot/Plot_Frame_Pipeline.h" #include "Core/plot/Plot_Core.h" #include "Core/execution/Render_Executor.h" #include "Core/plottable/Performance_Overlay_p.h" #include "Core/plottable/export.h" #include "Core/render/Image.h" #include #include #include #include #include #include namespace renderive { namespace { bool unique_roles(const Triple_Buffer_View& view) { std::array seen{}; for (std::uint8_t index : view.index) { if (index >= seen.size() || seen[index]) return false; seen[index] = true; } return true; } Frame_Lifecycle_Record render_frame(std::uint64_t begin_ns, std::uint64_t end_ns) { Frame_Lifecycle_Record frame; frame.render_role_enter_ns = begin_ns; frame.render_begin_ns = begin_ns; frame.render_end_ns = end_ns; return frame; } bool wait_until(std::function predicate) { auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); while (std::chrono::steady_clock::now() < deadline) { if (predicate()) return true; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } return predicate(); } std::shared_ptr performance_frame(std::uint64_t frame_id, int width = 240, int height = 80) { auto frame = std::make_shared(); frame->frame_id = frame_id; frame->input_version = frame_id + 1; frame->render_begin_ns = 10; frame->prepare_begin_ns = 11; frame->prepare_end_ns = 12; frame->draw_begin_ns = 13; frame->draw_end_ns = 14; frame->render_end_ns = 20; frame->transition_12_ns = 25; frame->paint_begin_ns = 30; frame->paint_end_ns = 40; frame->transition_32_ns = 45; frame->pixel_width = static_cast(width); frame->pixel_height = static_cast(height); frame->viewport_version = 1; frame->outcome = Frame_Outcome::Presented; frame->worker.executor_at_finish.worker_count = 4; frame->worker.executor_at_finish.active_workers = 1; frame->worker.executor_at_finish.completed_task_nodes = frame_id; return frame; } bool valid_utf8(const std::string& text) { for (std::size_t i = 0; i < text.size();) { unsigned char c = static_cast(text[i]); std::size_t n = 0; if ((c & 0x80u) == 0) n = 1; else if ((c & 0xE0u) == 0xC0u) n = 2; else if ((c & 0xF0u) == 0xE0u) n = 3; else if ((c & 0xF8u) == 0xF0u) n = 4; else return false; if (i + n > text.size()) return false; for (std::size_t j = 1; j < n; ++j) { if ((static_cast(text[i + j]) & 0xC0u) != 0x80u) return false; } i += n; } return true; } } // namespace TEST(Renderive_Canvas, TextUsesPenAndFillUsesBrush) { auto diagnostics = Canvas::default_font_diagnostics(); ASSERT_TRUE(diagnostics.face_valid); ASSERT_TRUE(diagnostics.font_valid); ASSERT_GT(diagnostics.test_width, 0.0); ASSERT_GT(diagnostics.test_height, 0.0); Image image(400, 100); image.fill(Color::transparent()); { Canvas canvas(image); canvas.set_brush(Brush{Color::white(), Brush_Style::Solid}); canvas.set_pen(Pen{Color::black()}); canvas.fill_rect(RectF{0.0, 0.0, 400.0, 100.0}); canvas.draw_text(PointF{10.0, 10.0}, "0123456789 ABC \xE4\xB8\xAD\xE6\x96\x87"); canvas.draw_text(RectF{10.0, 50.0, 300.0, 30.0}, Text_Align::Left | Text_Align::Top, "0123"); } EXPECT_EQ(premultiplied_to_color(image.row(2)[2]), Color::white()); int dark_pixels = 0; for (int y = 8; y < 82; ++y) { const Pixel* row = image.row(y); ASSERT_NE(row, nullptr); for (int x = 8; x < 320; ++x) { Color color = premultiplied_to_color(row[x]); if (color.a && color.r < 120 && color.g < 120 && color.b < 120) ++dark_pixels; } } EXPECT_GT(dark_pixels, 20); } TEST(Renderive_Triple_Buffer, RoleSwapsAreTryOnlyAndDeterministic) { Triple_Role_Buffer_Control control; control.reset(); EXPECT_TRUE(unique_roles(control.read_view())); auto first = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { return true; }); EXPECT_EQ(first.result, Triple_Buffer_Result::Success); EXPECT_TRUE(unique_roles(control.read_view())); auto painting = control.try_acquire_role(Frame_Painting); ASSERT_EQ(painting.result, Triple_Buffer_Result::Success); auto blocked = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { return true; }); EXPECT_EQ(blocked.result, Triple_Buffer_Result::Busy); control.unmark_use(painting.lease); for (int i = 0; i < 1000000; ++i) { auto a = control.try_swap_role(Frame_Rendering, Frame_Middle, [](const Triple_Buffer_View&) { return true; }); ASSERT_EQ(a.result, Triple_Buffer_Result::Success); auto b = control.try_swap_role(Frame_Middle, Frame_Painting, [](const Triple_Buffer_View&) { return true; }); ASSERT_EQ(b.result, Triple_Buffer_Result::Success); ASSERT_TRUE(unique_roles(control.read_view())); } } TEST(Renderive_Frame_Pipeline, PublishAndPaintUseMiddleVersionOnly) { Plot_Frame_Pipeline pipeline; auto lease = pipeline.try_acquire_render_frame(); ASSERT_TRUE(static_cast(lease)); ASSERT_TRUE(lease.frame->metadata); lease.frame->metadata->frame_id = 1; lease.frame->metadata->render_begin_ns = 10; lease.frame->metadata->render_end_ns = 20; lease.frame->version.store(1, std::memory_order_release); lease.reset(); auto published = pipeline.try_publish_rendered_frame(25); EXPECT_TRUE(pipeline.middle_frame_ready()); EXPECT_TRUE(published.request_present); EXPECT_EQ(published.dropped_frame_record, nullptr); auto painting = pipeline.try_acquire_paint_frame(30); EXPECT_TRUE(painting.has_new_frame); EXPECT_TRUE(static_cast(painting.lease)); EXPECT_FALSE(pipeline.middle_frame_ready()); } TEST(Renderive_Frame_Pipeline, PaintAcquireFailureKeepsPendingRequest) { Plot_Frame_Pipeline pipeline; auto lease = pipeline.try_acquire_render_frame(); ASSERT_TRUE(static_cast(lease)); ASSERT_TRUE(lease.frame->metadata); lease.frame->metadata->frame_id = 1; lease.frame->metadata->render_begin_ns = 10; lease.frame->metadata->render_end_ns = 20; lease.frame->version.store(1, std::memory_order_release); lease.reset(); auto published = pipeline.try_publish_rendered_frame(25); ASSERT_TRUE(published.request_present); EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire)); auto busy = pipeline.frame_control.try_acquire_role(Frame_Painting); ASSERT_EQ(busy.result, Triple_Buffer_Result::Success); auto failed = pipeline.try_acquire_paint_frame(30); EXPECT_TRUE(failed.paint_frame_unavailable); EXPECT_FALSE(failed.present_request_was_pending); EXPECT_TRUE(pipeline.paint_request_pending.load(std::memory_order_acquire)); pipeline.frame_control.unmark_use(busy.lease); auto consumed = pipeline.try_acquire_paint_frame(35); EXPECT_TRUE(consumed.present_request_was_pending); EXPECT_TRUE(static_cast(consumed.lease)); } TEST(Renderive_MPMCQueue, UsesTryOnlyBoundedCapacity) { rigtorp::MPMCQueue queue(2); EXPECT_TRUE(queue.try_push(1)); EXPECT_TRUE(queue.try_push(2)); EXPECT_FALSE(queue.try_push(3)); int value = 0; EXPECT_TRUE(queue.try_pop(value)); EXPECT_EQ(value, 1); EXPECT_TRUE(queue.try_pop(value)); EXPECT_EQ(value, 2); EXPECT_FALSE(queue.try_pop(value)); } TEST(Renderive_Frame_Feedback, SelectsRenderPaintAndUserSources) { Frame_Feedback_Controller controller; controller.set_max_render_fps(200.0); for (int i = 0; i < 4; ++i) { auto render_limited = render_frame(0, 20000000); controller.on_render_completed(render_limited); } EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Render); controller.reset(); controller.set_max_render_fps(200.0); for (int i = 0; i < 4; ++i) { auto frame = render_frame(0, 5000000); controller.on_render_completed(frame); frame.transition_12_ns = 0; frame.paint_begin_ns = 0; frame.paint_end_ns = 25000000; controller.on_frame_presented(frame); } EXPECT_EQ(controller.state().source, Refresh_Limit_Source::Paint); controller.reset(); controller.set_max_render_fps(30.0); for (int i = 0; i < 4; ++i) { auto frame = render_frame(0, 5000000); controller.on_render_completed(frame); frame.paint_begin_ns = 0; frame.paint_end_ns = 8000000; controller.on_frame_presented(frame); } EXPECT_EQ(controller.state().source, Refresh_Limit_Source::User); } TEST(Renderive_Render_Executor, FrameAdmissionIsBounded) { Render_Executor executor(Render_Runtime_Config{1}); std::atomic_bool entered{false}; std::atomic_bool release{false}; std::atomic_int completed{0}; auto first_stat = std::make_shared(); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task([&]() { entered.store(true, std::memory_order_release); while (!release.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); }), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), first_stat, 1, true })); ASSERT_TRUE(wait_until([&]() { return entered.load(std::memory_order_acquire); })); auto second_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), second_stat, 1, true })); auto third_stat = std::make_shared(); EXPECT_TRUE(executor.try_submit(Render_Executor_Task{ 3, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([&]() { completed.fetch_add(1, std::memory_order_acq_rel); }), third_stat, 1, true })); auto duplicate_stat = std::make_shared(); EXPECT_FALSE(executor.try_submit(Render_Executor_Task{ 2, 2, Render_Task_Kind::Frame, Task([]() {}), {}, Task([]() {}), duplicate_stat, 1, true })); auto overflow_stat = std::make_shared(); EXPECT_FALSE(executor.try_submit(Render_Executor_Task{ 4, 1, Render_Task_Kind::Frame, Task([]() {}), {}, Task([]() {}), overflow_stat, 1, true })); release.store(true, std::memory_order_release); ASSERT_TRUE(wait_until([&]() { return completed.load(std::memory_order_acquire) == 3; })); executor.shutdown(); } TEST(Renderive_Render_Executor, TaskflowTopologyRunsInFrameOrder) { Render_Executor executor(Render_Runtime_Config{2}); std::atomic_int sequence{0}; std::atomic_bool completed{false}; auto stat = std::make_shared(); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task{}, [&sequence](tf::Taskflow& taskflow, tf::Task entry, tf::Task exit) { auto prepare = taskflow.emplace([&sequence]() { int expected = 0; sequence.compare_exchange_strong(expected, 1, std::memory_order_acq_rel); }); auto draw = taskflow.emplace([&sequence]() { int expected = 1; sequence.compare_exchange_strong(expected, 2, std::memory_order_acq_rel); }); auto finalize = taskflow.emplace([&sequence]() { int expected = 2; sequence.compare_exchange_strong(expected, 3, std::memory_order_acq_rel); }); entry.precede(prepare); prepare.precede(draw); draw.precede(finalize); finalize.precede(exit); }, Task([&completed]() { completed.store(true, std::memory_order_release); }), stat, 3, true })); ASSERT_TRUE(wait_until([&]() { return completed.load(std::memory_order_acquire); })); EXPECT_EQ(sequence.load(std::memory_order_acquire), 3); EXPECT_EQ(stat->task_count, 3u); executor.shutdown(); } TEST(Renderive_Render_Executor, QueuedFramesRunRoundRobinByPlot) { Render_Executor executor(Render_Runtime_Config{1}); std::atomic_bool first_entered{false}; std::atomic_bool release_first{false}; std::atomic_int order_index{0}; std::array order{}; ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 1, 1, Render_Task_Kind::Frame, Task([&]() { first_entered.store(true, std::memory_order_release); while (!release_first.load(std::memory_order_acquire)) std::this_thread::sleep_for(std::chrono::milliseconds(1)); order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 1; }), {}, Task([]() {}), std::make_shared(), 1, true })); ASSERT_TRUE(wait_until([&]() { return first_entered.load(std::memory_order_acquire); })); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 2, 1, Render_Task_Kind::Frame, Task([&]() { order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 2; }), {}, Task([]() {}), std::make_shared(), 1, true })); ASSERT_TRUE(executor.try_submit(Render_Executor_Task{ 3, 1, Render_Task_Kind::Frame, Task([&]() { order[order_index.fetch_add(1, std::memory_order_acq_rel)] = 3; }), {}, Task([]() {}), std::make_shared(), 1, true })); release_first.store(true, std::memory_order_release); ASSERT_TRUE(wait_until([&]() { return order_index.load(std::memory_order_acquire) == 3; })); EXPECT_EQ(order[0], 1); EXPECT_EQ(order[1], 2); EXPECT_EQ(order[2], 3); executor.shutdown(); } TEST(Renderive_Performance_Shower, ToggleDoesNotReenterRenderableTreeUnsafely) { Plot_Core plot(std::make_unique()); plot.init(); auto root = plot.root_renderable(); ASSERT_TRUE(root); EXPECT_TRUE(root->children_snapshot().empty()); set_performance_overlay_enabled(plot, true); EXPECT_TRUE(performance_overlay_enabled(plot)); EXPECT_TRUE(root->children_snapshot().empty()); set_performance_overlay_enabled(plot, false); EXPECT_FALSE(performance_overlay_enabled(plot)); EXPECT_TRUE(root->children_snapshot().empty()); } TEST(Renderive_Performance_Shower, TogglePreservesAttachedOptions) { Plot_Core plot(std::make_unique()); plot.init(); Performance_Overlay_Options options; options.font.size = 17.0; options.background = Color{10, 20, 30, 255}; options.foreground = Color{240, 240, 240, 255}; auto original = attach_performance_overlay(plot, options); ASSERT_TRUE(original); set_performance_overlay_enabled(plot, true); EXPECT_TRUE(performance_overlay_enabled(plot)); set_performance_overlay_enabled(plot, false); EXPECT_FALSE(performance_overlay_enabled(plot)); set_performance_overlay_enabled(plot, true); auto toggled = performance_overlay(plot); ASSERT_EQ(toggled, original); EXPECT_EQ(toggled->options().font.size, options.font.size); EXPECT_EQ(toggled->options().background, options.background); EXPECT_EQ(toggled->options().foreground, options.foreground); } TEST(Renderive_Hover_Info, DefaultTextPenContrastsWithDefaultBackground) { Hover_Tooltip_State state; EXPECT_EQ(state.tooltip_background_brush.color, Color::white()); EXPECT_EQ(state.tooltip_text_pen.color, Color::black()); } TEST(Renderive_Performance_Shower, WorkerConsumesSharedFrameRecord) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto frame = std::make_shared(); frame->frame_id = 7; frame->input_version = 3; frame->render_begin_ns = 10; frame->render_end_ns = 20; frame->transition_12_ns = 25; frame->paint_begin_ns = 30; frame->paint_end_ns = 40; frame->transition_32_ns = 45; frame->pixel_width = 320; frame->pixel_height = 240; frame->outcome = Frame_Outcome::Presented; Performance_Frame_Record record = frame; ASSERT_TRUE(state->metric_queue.try_push(record)); schedule_performance_metrics_worker(state); auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(2); std::shared_ptr snapshot; while (std::chrono::steady_clock::now() < deadline) { snapshot = state->published_snapshot.load(std::memory_order_acquire); if (snapshot && !state->worker_active.load(std::memory_order_acquire)) break; std::this_thread::sleep_for(std::chrono::milliseconds(1)); } state->cancelled.store(true, std::memory_order_release); ASSERT_TRUE(snapshot); EXPECT_FALSE(snapshot->lines.empty()); EXPECT_TRUE(state->aggregate.has_frame); EXPECT_EQ(state->aggregate.latest.frame_id, 7); } TEST(Renderive_Performance_Shower, OptionsAffectSnapshotRendering) { Performance_Overlay shower; Performance_Overlay_Options options; options.font.size = 18.0; options.background = Color{10, 20, 30, 255}; options.foreground = Color{240, 240, 240, 255}; shower.set_options(options); shower.set_enabled(true); shower.set_viewport(Size{260, 120}, 1); shower.collect_frame(performance_frame(1, 260, 120)); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->image.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); ASSERT_FALSE(snapshot->image.empty()); EXPECT_EQ(shower.options().font.size, 18.0); EXPECT_GT(snapshot->line_height, 12.0); EXPECT_EQ(premultiplied_to_color(snapshot->image.row(1)[1]), options.background); } TEST(Renderive_Performance_Shower, OutcomeWidthIsSeededWithLongestEnumText) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto outcome_width = [&]() { return state->field_layout_states[static_cast(Performance_Metric_Id::Outcome)].max_value_width; }; auto presented = performance_frame(1, 260, 120); presented->outcome = Frame_Outcome::Presented; ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{presented})); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return state->aggregate.latest.frame_id == 1 && !state->worker_active.load(std::memory_order_acquire); })); double seeded_width = outcome_width(); EXPECT_GT(seeded_width, 0.0); auto dropped = performance_frame(2, 260, 120); dropped->outcome = Frame_Outcome::Worker_Budget_Dropped; ASSERT_TRUE(state->metric_queue.try_push(Performance_Frame_Record{dropped})); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return state->aggregate.latest.frame_id == 2 && !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(outcome_width(), seeded_width); state->cancelled.store(true, std::memory_order_release); } TEST(Renderive_Performance_Shower, NumericFieldWidthIsMonotonicAcrossResetAndResize) { auto state = std::make_shared(); state->enabled.store(true, std::memory_order_release); auto frame_width = [&]() { return state->field_layout_states[static_cast(Performance_Metric_Id::Frame_Id)].max_value_width; }; double previous = 0.0; double wide = 0.0; for (std::uint64_t frame_id : {1ull, 99ull, 123456ull, 2ull, 8ull}) { Performance_Frame_Record record = performance_frame(frame_id, 320, 120); ASSERT_TRUE(state->metric_queue.try_push(record)); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { auto snapshot = state->published_snapshot.load(std::memory_order_acquire); return snapshot && state->aggregate.latest.frame_id == frame_id && !state->worker_active.load(std::memory_order_acquire); })); double current = frame_width(); EXPECT_GE(current, previous); previous = current; if (frame_id == 123456) wide = current; if (frame_id == 2 || frame_id == 8) EXPECT_EQ(current, wide); } state->reset_requested.store(true, std::memory_order_release); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { return !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(frame_width(), wide); state->viewport_width.store(180, std::memory_order_release); state->viewport_height.store(90, std::memory_order_release); state->viewport_version.store(2, std::memory_order_release); state->snapshot_rebuild_requested.store(true, std::memory_order_release); schedule_performance_metrics_worker(state); ASSERT_TRUE(wait_until([&]() { auto snapshot = state->published_snapshot.load(std::memory_order_acquire); return snapshot && snapshot->viewport_version == 2 && !state->worker_active.load(std::memory_order_acquire); })); EXPECT_EQ(frame_width(), wide); state->cancelled.store(true, std::memory_order_release); } TEST(Renderive_Performance_Shower, ScrollCommandsUpdateOffsetAndClamp) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{220, 70}, 1); auto frame = performance_frame(1, 220, 70); shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && snapshot->max_scroll_offset > 0.0; })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); EXPECT_TRUE(shower.handle_wheel(PointF{5.0, 5.0}, -120.0, 0.0)); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->scroll_offset > snapshot->scroll_offset; })); snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); PointF page_down{snapshot->scroll_track_rect.x + 1.0, snapshot->scroll_thumb_rect.bottom() + 1.0}; EXPECT_TRUE(shower.handle_pointer_press(page_down)); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->scroll_offset > snapshot->scroll_offset; })); snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); PointF thumb_center{snapshot->scroll_thumb_rect.x + 1.0, snapshot->scroll_thumb_rect.y + snapshot->scroll_thumb_rect.height * 0.5}; EXPECT_TRUE(shower.handle_pointer_press(thumb_center)); EXPECT_TRUE(shower.handle_pointer_move(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); EXPECT_TRUE(shower.handle_pointer_release(PointF{thumb_center.x, snapshot->scroll_track_rect.bottom()})); ASSERT_TRUE(wait_until([&]() { auto next = shower.display_snapshot(); return next && next->max_scroll_offset > 0.0 && next->scroll_offset > next->max_scroll_offset * 0.8; })); } TEST(Renderive_Performance_Shower, Utf8WrappingKeepsValidText) { Performance_Overlay shower; shower.set_enabled(true); shower.set_viewport(Size{120, 90}, 1); auto frame = performance_frame(1, 120, 90); Renderable_Frame_Stat stat; stat.object_name = "\xE4\xB8\xAD\xE6\x96\x87\xE6\xB5\x8B\xE8\xAF\x95\xE5\xAD\x97\xE6\xAE\xB5\xE5\xBE\x88\xE9\x95\xBF\xE5\xBE\x88\xE9\x95\xBF"; stat.tree_depth = 0; stat.render_count = 1; stat.total_render_time_ns = 1000000; frame->renderable_stats.push_back(stat); shower.collect_frame(frame); ASSERT_TRUE(wait_until([&]() { auto snapshot = shower.display_snapshot(); return snapshot && !snapshot->lines.empty(); })); auto snapshot = shower.display_snapshot(); ASSERT_TRUE(snapshot); for (const auto& line : snapshot->lines) EXPECT_TRUE(valid_utf8(line)) << line; } } // namespace renderive int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); }